├── .husky
├── pre-commit
└── commit-msg
├── bun.lockb
├── public
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
├── 6.png
├── 7.png
├── 8.png
├── 9.png
├── 10.png
├── p1.png
├── p2.png
├── p3.png
├── p4.png
├── p5.png
├── p6.png
├── notion.png
├── slack.png
├── discord.png
├── fuzora-logo.png
├── googleDrive.png
├── temp-banner.png
├── promemberscall.png
├── fuzora-thumbnails.png
├── vercel.svg
└── next.svg
├── src
├── app
│ ├── favicon.ico
│ ├── (auth)
│ │ ├── sign-in
│ │ │ └── [[...sign-in]]
│ │ │ │ └── page.tsx
│ │ ├── sign-up
│ │ │ └── [[...sign-up]]
│ │ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── (main)
│ │ ├── (pages)
│ │ │ ├── workflows
│ │ │ │ ├── editor
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ └── [editorId]
│ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ ├── _actions
│ │ │ │ │ │ └── workflow-connections.tsx
│ │ │ │ │ │ └── _components
│ │ │ │ │ │ ├── render-output-accordian.tsx
│ │ │ │ │ │ ├── custom-handle.tsx
│ │ │ │ │ │ ├── google-file-details.tsx
│ │ │ │ │ │ ├── editor-canvas-card-icon-hepler.tsx
│ │ │ │ │ │ ├── flow-instance.tsx
│ │ │ │ │ │ ├── editor-canvas-card-single.tsx
│ │ │ │ │ │ ├── render-connection-accordion.tsx
│ │ │ │ │ │ ├── google-drive-files.tsx
│ │ │ │ │ │ ├── content-based-on-title.tsx
│ │ │ │ │ │ └── editor-canvas-sidebar.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ ├── _components
│ │ │ │ │ ├── more-creadits.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── workflow-button.tsx
│ │ │ │ │ └── workflow.tsx
│ │ │ │ └── _actions
│ │ │ │ │ └── workflow-connections.tsx
│ │ │ ├── dashboard
│ │ │ │ └── page.tsx
│ │ │ ├── connections
│ │ │ │ ├── _actions
│ │ │ │ │ ├── get-user.tsx
│ │ │ │ │ ├── google-connection.tsx
│ │ │ │ │ ├── notion-connection.tsx
│ │ │ │ │ ├── discord-connection.tsx
│ │ │ │ │ └── slack-connection.tsx
│ │ │ │ ├── _components
│ │ │ │ │ └── connection-card.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── billing
│ │ │ │ ├── _actions
│ │ │ │ │ └── payment-connecetions.tsx
│ │ │ │ ├── _components
│ │ │ │ │ ├── creadits-tracker.tsx
│ │ │ │ │ ├── subscription-card.tsx
│ │ │ │ │ └── billing-dashboard.tsx
│ │ │ │ └── page.tsx
│ │ │ └── settings
│ │ │ │ ├── _components
│ │ │ │ ├── uploadcare-button.tsx
│ │ │ │ └── profile-picture.tsx
│ │ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── api
│ │ ├── payment
│ │ │ └── route.ts
│ │ ├── drive
│ │ │ └── route.ts
│ │ ├── auth
│ │ │ └── callback
│ │ │ │ ├── discord
│ │ │ │ └── route.ts
│ │ │ │ ├── notion
│ │ │ │ └── route.ts
│ │ │ │ └── slack
│ │ │ │ └── route.tsx
│ │ ├── drive-activity
│ │ │ ├── route.ts
│ │ │ └── notification
│ │ │ │ └── route.ts
│ │ └── clerk-webhook
│ │ │ └── route.ts
│ ├── layout.tsx
│ └── globals.css
├── lib
│ ├── utils.ts
│ ├── db.ts
│ ├── types.ts
│ └── editor-utils.ts
├── providers
│ ├── theme-provider.tsx
│ ├── billing-provider.tsx
│ ├── modal-provider.tsx
│ ├── connections-provider.tsx
│ └── editor-provider.tsx
├── components
│ ├── ui
│ │ ├── label.tsx
│ │ ├── separator.tsx
│ │ ├── input.tsx
│ │ ├── progress.tsx
│ │ ├── sonner.tsx
│ │ ├── badge.tsx
│ │ ├── switch.tsx
│ │ ├── tooltip.tsx
│ │ ├── popover.tsx
│ │ ├── resizable.tsx
│ │ ├── button.tsx
│ │ ├── tabs.tsx
│ │ ├── accordion.tsx
│ │ ├── card.tsx
│ │ ├── drawer.tsx
│ │ ├── dialog.tsx
│ │ └── form.tsx
│ ├── icons
│ │ ├── workflows.tsx
│ │ ├── home.tsx
│ │ ├── payment.tsx
│ │ ├── cloud_download.tsx
│ │ ├── category.tsx
│ │ ├── clipboard.tsx
│ │ └── settings.tsx
│ ├── global
│ │ ├── mode-toggle.tsx
│ │ ├── custom-modal.tsx
│ │ ├── navbar.tsx
│ │ ├── container-scroll-animation.tsx
│ │ ├── infinite-moving-cards.tsx
│ │ ├── 3d-card.tsx
│ │ ├── connect-parallax.tsx
│ │ └── lamp.tsx
│ ├── infobar
│ │ └── index.tsx
│ ├── forms
│ │ ├── profile-form.tsx
│ │ └── workflow-form.tsx
│ └── sidebar
│ │ └── index.tsx
├── store.tsx
└── middleware.ts
├── postcss.config.js
├── commitlint.config.ts
├── .vscode
└── settings.json
├── next.config.mjs
├── .github
└── workflow
│ └── code-quality.yml
├── components.json
├── .gitignore
├── tsconfig.json
├── biome.json
├── .env.example
├── package.json
├── prisma
└── schema.prisma
└── tailwind.config.ts
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | lint-staged
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | npx --no -- commitlint --edit $1
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/bun.lockb
--------------------------------------------------------------------------------
/public/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/1.png
--------------------------------------------------------------------------------
/public/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/2.png
--------------------------------------------------------------------------------
/public/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/3.png
--------------------------------------------------------------------------------
/public/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/4.png
--------------------------------------------------------------------------------
/public/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/5.png
--------------------------------------------------------------------------------
/public/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/6.png
--------------------------------------------------------------------------------
/public/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/7.png
--------------------------------------------------------------------------------
/public/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/8.png
--------------------------------------------------------------------------------
/public/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/9.png
--------------------------------------------------------------------------------
/public/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/10.png
--------------------------------------------------------------------------------
/public/p1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/p1.png
--------------------------------------------------------------------------------
/public/p2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/p2.png
--------------------------------------------------------------------------------
/public/p3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/p3.png
--------------------------------------------------------------------------------
/public/p4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/p4.png
--------------------------------------------------------------------------------
/public/p5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/p5.png
--------------------------------------------------------------------------------
/public/p6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/p6.png
--------------------------------------------------------------------------------
/public/notion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/notion.png
--------------------------------------------------------------------------------
/public/slack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/slack.png
--------------------------------------------------------------------------------
/public/discord.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/discord.png
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/public/fuzora-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/fuzora-logo.png
--------------------------------------------------------------------------------
/public/googleDrive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/googleDrive.png
--------------------------------------------------------------------------------
/public/temp-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/temp-banner.png
--------------------------------------------------------------------------------
/public/promemberscall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/promemberscall.png
--------------------------------------------------------------------------------
/public/fuzora-thumbnails.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anayatkhan1/Fuzora/HEAD/public/fuzora-thumbnails.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/nextjs";
2 |
3 | export default function Page() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/commitlint.config.ts:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@commitlint/config-conventional"],
3 | rules: {
4 | "type-enum": [2, "always", ["feat", "fix", "wip", "patch", "build"]],
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "biomejs.biome",
3 | "editor.formatOnSave": true,
4 | "editor.codeActionsOnSave": {
5 | "quickfix.biome": "explicit"
6 | },
7 | "[json]": {
8 | "editor.defaultFormatter": "biomejs.biome"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/page.tsx:
--------------------------------------------------------------------------------
1 | type Props = {};
2 |
3 | const Page = (props: Props) => {
4 | //CHALLENGE: If the user tries to access this route you should send them to their first workflow they have or create one or you can have your own behavior.
5 | return
Page
;
6 | };
7 |
8 | export default Page;
9 |
--------------------------------------------------------------------------------
/src/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type Props = { children: React.ReactNode };
4 |
5 | const Layout = ({ children }: Props) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
13 | export default Layout;
14 |
--------------------------------------------------------------------------------
/src/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { ThemeProvider as NextThemesProvider } from "next-themes";
3 | import { type ThemeProviderProps } from "next-themes/dist/types";
4 |
5 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
6 | return {children} ;
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | const DashboardPage = () => {
2 | return (
3 |
4 |
5 | Dashboard
6 |
7 |
8 | );
9 | };
10 |
11 | export default DashboardPage;
12 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/connections/_actions/get-user.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/db";
4 |
5 | export const getUserData = async (id: string) => {
6 | const user_info = await db.user.findUnique({
7 | where: {
8 | clerkId: id,
9 | },
10 | include: {
11 | connections: true,
12 | },
13 | });
14 |
15 | return user_info;
16 | };
17 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: 'https',
7 | hostname: 'img.clerk.com',
8 | },
9 | {
10 | protocol: 'https',
11 | hostname: 'ucarecdn.com',
12 | },
13 | ],
14 | },
15 | }
16 |
17 | export default nextConfig
18 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type Props = { children: React.ReactNode };
4 |
5 | const Layout = ({ children }: Props) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
13 | export default Layout;
14 |
--------------------------------------------------------------------------------
/.github/workflow/code-quality.yml:
--------------------------------------------------------------------------------
1 | name: Code quality
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | quality:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v4
13 | - name: Setup Biome
14 | uses: biomejs/setup-biome@v2
15 | with:
16 | version: latest
17 | - name: Run Biome
18 | run: biome ci src
--------------------------------------------------------------------------------
/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": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/src/app/(main)/layout.tsx:
--------------------------------------------------------------------------------
1 | import InfoBar from "@/components/infobar";
2 | import Sidebar from "@/components/sidebar";
3 | import React from "react";
4 |
5 | type Props = { children: React.ReactNode };
6 |
7 | const Layout = (props: Props) => {
8 | return (
9 |
10 |
11 |
12 |
13 | {props.children}
14 |
15 |
16 | );
17 | };
18 |
19 | export default Layout;
20 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/page.tsx:
--------------------------------------------------------------------------------
1 | import Workflows from "./_components";
2 | import WorkflowButton from "./_components/workflow-button";
3 |
4 | type Props = {};
5 |
6 | const Page = (props: Props) => {
7 | return (
8 |
9 |
10 | Workflows
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | export default Page;
19 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/billing/_actions/payment-connecetions.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/db";
4 | import { currentUser } from "@clerk/nextjs/server";
5 |
6 | export const onPaymentDetails = async () => {
7 | const user = await currentUser();
8 |
9 | if (user) {
10 | const connection = await db.user.findFirst({
11 | where: {
12 | clerkId: user.id,
13 | },
14 | select: {
15 | tier: true,
16 | credits: true,
17 | },
18 | });
19 |
20 | if (user) {
21 | return connection;
22 | }
23 | }
24 | };
25 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { ConnectionsProvider } from "@/providers/connections-provider";
2 | import EditorProvider from "@/providers/editor-provider";
3 | import EditorCanvas from "./_components/editor-canvas";
4 |
5 | type Props = {};
6 |
7 | const Page = (props: Props) => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default Page;
20 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/_components/more-creadits.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Card, CardContent, CardDescription } from "@/components/ui/card";
3 | import { useBilling } from "@/providers/billing-provider";
4 |
5 | type Props = {};
6 |
7 | const MoreCredits = (props: Props) => {
8 | const { credits } = useBilling();
9 | return credits !== "0" ? (
10 | <>>
11 | ) : (
12 |
13 |
14 | You are out of credits
15 |
16 |
17 | );
18 | };
19 |
20 | export default MoreCredits;
21 |
--------------------------------------------------------------------------------
/.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
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | certificates
40 |
41 | package-lock.json
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | "@/*": ["./src/*"]
22 | },
23 | "types": ["@uploadcare/blocks/types/jsx"]
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/_components/index.tsx:
--------------------------------------------------------------------------------
1 | import { onGetWorkflows } from "../_actions/workflow-connections";
2 | import MoreCredits from "./more-creadits";
3 | import Workflow from "./workflow";
4 |
5 | type Props = {};
6 |
7 | const Workflows = async (props: Props) => {
8 | const workflows = await onGetWorkflows();
9 | return (
10 |
11 |
12 |
13 | {workflows?.length ? (
14 | workflows.map((flow) => )
15 | ) : (
16 |
17 | No Workflows
18 |
19 | )}
20 |
21 |
22 | );
23 | };
24 |
25 | export default Workflows;
26 |
--------------------------------------------------------------------------------
/src/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { Pool, neonConfig } from "@neondatabase/serverless";
2 | import { PrismaNeon } from "@prisma/adapter-neon";
3 | import { PrismaClient } from "@prisma/client";
4 | import ws from "ws";
5 |
6 | const prismaClientSingleton = () => {
7 | neonConfig.webSocketConstructor = ws;
8 | const connectionString = `${process.env.DATABASE_URL}`;
9 |
10 | const pool = new Pool({ connectionString });
11 | const adapter = new PrismaNeon(pool);
12 | const prisma = new PrismaClient({ adapter });
13 |
14 | return prisma;
15 | };
16 |
17 | declare const globalThis: {
18 | prismaGlobal: ReturnType;
19 | } & typeof global;
20 |
21 | export const db = globalThis.prismaGlobal ?? prismaClientSingleton();
22 |
23 | if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;
24 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label";
4 | import { type VariantProps, cva } from "class-variance-authority";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
4 | import * as React from "react";
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 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_actions/workflow-connections.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/db";
4 |
5 | export const onCreateNodesEdges = async (
6 | flowId: string,
7 | nodes: string,
8 | edges: string,
9 | flowPath: string,
10 | ) => {
11 | const flow = await db.workflows.update({
12 | where: {
13 | id: flowId,
14 | },
15 | data: {
16 | nodes,
17 | edges,
18 | flowPath: flowPath,
19 | },
20 | });
21 |
22 | if (flow) return { message: "flow saved" };
23 | };
24 |
25 | export const onFlowPublish = async (workflowId: string, state: boolean) => {
26 | console.log(state);
27 | const published = await db.workflows.update({
28 | where: {
29 | id: workflowId,
30 | },
31 | data: {
32 | publish: state,
33 | },
34 | });
35 |
36 | if (published.publish) return "Workflow published";
37 | return "Workflow unpublished";
38 | };
39 |
--------------------------------------------------------------------------------
/src/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 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as ProgressPrimitive from "@radix-ui/react-progress";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ));
26 | Progress.displayName = ProgressPrimitive.Root.displayName;
27 |
28 | export { Progress };
29 |
--------------------------------------------------------------------------------
/src/components/icons/workflows.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | type Props = { selected: boolean };
4 |
5 | const Workflows = ({ selected }: Props) => {
6 | return (
7 |
14 |
21 |
22 | );
23 | };
24 |
25 | export default Workflows;
26 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster as Sonner } from "sonner";
5 |
6 | type ToasterProps = React.ComponentProps;
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme();
10 |
11 | return (
12 |
28 | );
29 | };
30 |
31 | export { Toaster };
32 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/render-output-accordian.tsx:
--------------------------------------------------------------------------------
1 | import { ConnectionProviderProps } from "@/providers/connections-provider";
2 | import { EditorState } from "@/providers/editor-provider";
3 | import { useFuzzieStore } from "@/store";
4 | import ContentBasedOnTitle from "./content-based-on-title";
5 |
6 | type Props = {
7 | state: EditorState;
8 | nodeConnection: ConnectionProviderProps;
9 | };
10 |
11 | const RenderOutputAccordion = ({ state, nodeConnection }: Props) => {
12 | const {
13 | googleFile,
14 | setGoogleFile,
15 | selectedSlackChannels,
16 | setSelectedSlackChannels,
17 | } = useFuzzieStore();
18 | return (
19 |
27 | );
28 | };
29 |
30 | export default RenderOutputAccordion;
31 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/billing/_components/creadits-tracker.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardTitle } from "@/components/ui/card";
2 | import { Progress } from "@/components/ui/progress";
3 |
4 | type Props = {
5 | credits: number;
6 | tier: string;
7 | };
8 |
9 | const CreditTracker = ({ credits, tier }: Props) => {
10 | return (
11 |
12 |
13 |
14 | Credit Tracker
15 |
25 |
26 |
27 | {credits}/
28 | {tier == "Free" ? 10 : tier == "Pro" ? 100 : "Unlimited"}
29 |
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default CreditTracker;
38 |
--------------------------------------------------------------------------------
/src/store.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 |
3 | export interface Option {
4 | value: string;
5 | label: string;
6 | disable?: boolean;
7 | /** fixed option that can't be removed. */
8 | fixed?: boolean;
9 | /** Group the options by providing key. */
10 | [key: string]: string | boolean | undefined;
11 | }
12 |
13 | type FuzzieStore = {
14 | googleFile: any;
15 | setGoogleFile: (googleFile: any) => void;
16 | slackChannels: Option[];
17 | setSlackChannels: (slackChannels: Option[]) => void;
18 | selectedSlackChannels: Option[];
19 | setSelectedSlackChannels: (selectedSlackChannels: Option[]) => void;
20 | };
21 |
22 | export const useFuzzieStore = create()((set) => ({
23 | googleFile: {},
24 | setGoogleFile: (googleFile: any) => set({ googleFile }),
25 | slackChannels: [],
26 | setSlackChannels: (slackChannels: Option[]) => set({ slackChannels }),
27 | selectedSlackChannels: [],
28 | setSelectedSlackChannels: (selectedSlackChannels: Option[]) =>
29 | set({ selectedSlackChannels }),
30 | }));
31 |
--------------------------------------------------------------------------------
/src/app/api/payment/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import Stripe from "stripe";
3 |
4 | export async function GET(req: NextRequest) {
5 | const stripe = new Stripe(process.env.STRIPE_SECRET!, {
6 | typescript: true,
7 | apiVersion: "2023-10-16",
8 | });
9 |
10 | const products = await stripe.prices.list({
11 | limit: 3,
12 | });
13 |
14 | return NextResponse.json(products.data);
15 | }
16 |
17 | export async function POST(req: NextRequest) {
18 | const stripe = new Stripe(process.env.STRIPE_SECRET!, {
19 | typescript: true,
20 | apiVersion: "2023-10-16",
21 | });
22 | const data = await req.json();
23 | const session = await stripe.checkout.sessions.create({
24 | line_items: [
25 | {
26 | price: data.priceId,
27 | quantity: 1,
28 | },
29 | ],
30 | mode: "subscription",
31 | success_url: `${process.env.NEXT_PUBLIC_URL}/billing?session_id={CHECKOUT_SESSION_ID}`,
32 | cancel_url: `${process.env.NEXT_PUBLIC_URL}/billing`,
33 | });
34 | return NextResponse.json(session.url);
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/connections/_actions/google-connection.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { clerkClient } from "@clerk/nextjs/server";
3 | import { auth } from "@clerk/nextjs/server";
4 | import { google } from "googleapis";
5 |
6 | export const getFileMetaData = async () => {
7 | "use server";
8 | const oauth2Client = new google.auth.OAuth2(
9 | process.env.GOOGLE_CLIENT_ID,
10 | process.env.GOOGLE_CLIENT_SECRET,
11 | process.env.OAUTH2_REDIRECT_URI,
12 | );
13 |
14 | const { userId } = auth();
15 | const clerk = clerkClient();
16 |
17 | if (!userId) {
18 | return { message: "User not found" };
19 | }
20 |
21 | const clerkResponse = await clerk.users.getUserOauthAccessToken(
22 | userId,
23 | "oauth_google",
24 | );
25 |
26 | const accessToken = clerkResponse.data[0].token;
27 |
28 | oauth2Client.setCredentials({
29 | access_token: accessToken,
30 | });
31 |
32 | const drive = google.drive({ version: "v3", auth: oauth2Client });
33 | const response = await drive.files.list();
34 |
35 | if (response) {
36 | return response.data;
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/src/providers/billing-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 |
5 | type BillingProviderProps = {
6 | credits: string;
7 | tier: string;
8 | setCredits: React.Dispatch>;
9 | setTier: React.Dispatch>;
10 | };
11 |
12 | const initialValues: BillingProviderProps = {
13 | credits: "",
14 | setCredits: () => undefined,
15 | tier: "",
16 | setTier: () => undefined,
17 | };
18 |
19 | type WithChildProps = {
20 | children: React.ReactNode;
21 | };
22 |
23 | const context = React.createContext(initialValues);
24 | const { Provider } = context;
25 |
26 | export const BillingProvider = ({ children }: WithChildProps) => {
27 | const [credits, setCredits] = React.useState(initialValues.credits);
28 | const [tier, setTier] = React.useState(initialValues.tier);
29 |
30 | const values = {
31 | credits,
32 | setCredits,
33 | tier,
34 | setTier,
35 | };
36 |
37 | return {children} ;
38 | };
39 |
40 | export const useBilling = () => {
41 | const state = React.useContext(context);
42 | return state;
43 | };
44 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/_components/workflow-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Workflowform from "@/components/forms/workflow-form";
3 | import CustomModal from "@/components/global/custom-modal";
4 | import { Button } from "@/components/ui/button";
5 | import { useBilling } from "@/providers/billing-provider";
6 | import { useModal } from "@/providers/modal-provider";
7 | import { Plus } from "lucide-react";
8 |
9 | type Props = {};
10 |
11 | const WorkflowButton = (props: Props) => {
12 | const { setOpen, setClose } = useModal();
13 | const { credits } = useBilling();
14 |
15 | const handleClick = () => {
16 | setOpen(
17 |
21 |
22 | ,
23 | );
24 | };
25 |
26 | return (
27 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default WorkflowButton;
43 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { type VariantProps, cva } from "class-variance-authority";
2 | import * as React from "react";
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 |
--------------------------------------------------------------------------------
/src/components/icons/home.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | type Props = { selected: boolean };
4 |
5 | const Home = ({ selected }: Props) => {
6 | return (
7 |
14 |
23 |
30 |
31 | );
32 | };
33 |
34 | export default Home;
35 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as SwitchPrimitives from "@radix-ui/react-switch";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
4 | import * as React from "react";
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 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/custom-handle.tsx:
--------------------------------------------------------------------------------
1 | import { useEditor } from "@/providers/editor-provider";
2 | import { CSSProperties } from "react";
3 | import { Handle, HandleProps } from "reactflow";
4 |
5 | type Props = HandleProps & { style?: CSSProperties };
6 |
7 | const selector = (s: any) => ({
8 | nodeInternals: s.nodeInternals,
9 | edges: s.edges,
10 | });
11 |
12 | const CustomHandle = (props: Props) => {
13 | const { state } = useEditor();
14 |
15 | return (
16 | {
19 | const sourcesFromHandleInState = state.editor.edges.filter(
20 | (edge) => edge.source === e.source,
21 | ).length;
22 | const sourceNode = state.editor.elements.find(
23 | (node) => node.id === e.source,
24 | );
25 | //target
26 | const targetFromHandleInState = state.editor.edges.filter(
27 | (edge) => edge.target === e.target,
28 | ).length;
29 |
30 | if (targetFromHandleInState === 1) return false;
31 | if (sourceNode?.type === "Condition") return true;
32 | if (sourcesFromHandleInState < 1) return true;
33 | return false;
34 | }}
35 | className="!-bottom-2 !h-4 !w-4 dark:bg-neutral-800"
36 | />
37 | );
38 | };
39 |
40 | export default CustomHandle;
41 |
--------------------------------------------------------------------------------
/src/components/global/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Moon, Sun } from "lucide-react";
3 | import { useTheme } from "next-themes";
4 |
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from "@/components/ui/dropdown-menu";
12 |
13 | export function ModeToggle() {
14 | const { setTheme } = useTheme();
15 | return (
16 |
17 |
18 |
19 |
20 |
21 | Toggle theme
22 |
23 |
24 |
25 | setTheme("light")}>
26 | Light
27 |
28 | setTheme("dark")}>
29 | Dark
30 |
31 | setTheme("system")}>
32 | System
33 |
34 |
35 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json",
3 | "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
4 | "files": { "ignoreUnknown": false, "ignore": [] },
5 | "formatter": { "enabled": true, "indentStyle": "tab" },
6 | "organizeImports": { "enabled": true },
7 | "linter": {
8 | "enabled": true,
9 | "rules": {
10 | "recommended": false,
11 | "a11y": {
12 | "noAriaUnsupportedElements": "warn",
13 | "noBlankTarget": "off",
14 | "useAltText": "warn",
15 | "useAriaPropsForRole": "warn",
16 | "useValidAriaProps": "warn",
17 | "useValidAriaValues": "warn"
18 | },
19 | "correctness": {
20 | "noChildrenProp": "error",
21 | "useExhaustiveDependencies": "off",
22 | "useHookAtTopLevel": "error",
23 | "useJsxKeyInIterable": "error",
24 | "noUnusedImports": "warn"
25 | },
26 | "security": { "noDangerouslySetInnerHtmlWithChildren": "error" },
27 | "suspicious": {
28 | "noCommentText": "error",
29 | "noDuplicateJsxProps": "error"
30 | },
31 | "nursery": {
32 | "useSortedClasses": {
33 | "level": "warn",
34 | "fix": "safe"
35 | }
36 | }
37 | }
38 | },
39 | "javascript": { "formatter": { "quoteStyle": "double" } },
40 | "overrides": [{ "include": ["**/*.ts?(x)"] }]
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/settings/_components/uploadcare-button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as LR from "@uploadcare/blocks";
3 | import { useRouter } from "next/navigation";
4 | import { useEffect, useRef } from "react";
5 |
6 | type Props = {
7 | onUpload: (e: string) => any;
8 | };
9 |
10 | LR.registerBlocks(LR);
11 |
12 | const UploadCareButton = ({ onUpload }: Props) => {
13 | const router = useRouter();
14 | const ctxProviderRef = useRef<
15 | typeof LR.UploadCtxProvider.prototype & LR.UploadCtxProvider
16 | >(null);
17 |
18 | useEffect(() => {
19 | const handleUpload = async (e: any) => {
20 | const file = await onUpload(e.detail.cdnUrl);
21 | if (file) {
22 | router.refresh();
23 | }
24 | };
25 | ctxProviderRef?.current?.addEventListener(
26 | "file-upload-success",
27 | handleUpload,
28 | );
29 | }, []);
30 |
31 | return (
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default UploadCareButton;
48 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as PopoverPrimitive from "@radix-ui/react-popover";
4 | import * as React from "react";
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 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2 |
3 | const publicRoutes = [
4 | "/",
5 | "/api/clerk-webhook",
6 | "/api/drive-activity/notification",
7 | "/api/payment/success",
8 | ];
9 |
10 | const ignoredRoutes = [
11 | "/api/auth/callback/discord",
12 | "/api/auth/callback/notion",
13 | "/api/auth/callback/slack",
14 | "/api/flow",
15 | "/api/cron/wait",
16 | ];
17 |
18 | const isProtectedRoute = createRouteMatcher([
19 | "/dashboard(.*)",
20 | "/billing(.*)",
21 | "/connections(.*)",
22 | "/settings(.*)",
23 | "/workflows(.*)",
24 | ]);
25 |
26 | export default clerkMiddleware((auth, req) => {
27 | if (ignoredRoutes.some((route) => req.url.startsWith(route))) {
28 | return;
29 | }
30 | if (publicRoutes.some((route) => req.url.startsWith(route))) {
31 | return;
32 | }
33 | if (isProtectedRoute(req)) auth().protect();
34 | });
35 |
36 | export const config = {
37 | matcher: [
38 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
39 | "/(api|trpc)(.*)",
40 | ],
41 | };
42 |
43 | // https://www.googleapis.com/auth/userinfo.email
44 | // https://www.googleapis.com/auth/userinfo.profile
45 | // https://www.googleapis.com/auth/drive.activity.readonly
46 | // https://www.googleapis.com/auth/drive.metadata
47 | // https://www.googleapis.com/auth/drive.readonly
48 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/settings/_components/profile-picture.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Button } from "@/components/ui/button";
3 | import { X } from "lucide-react";
4 | import Image from "next/image";
5 | import { useRouter } from "next/navigation";
6 | import UploadCareButton from "./uploadcare-button";
7 |
8 | type Props = {
9 | userImage: string | null;
10 | onDelete?: any;
11 | onUpload: any;
12 | };
13 |
14 | const ProfilePicture = ({ userImage, onDelete, onUpload }: Props) => {
15 | const router = useRouter();
16 |
17 | const onRemoveProfileImage = async () => {
18 | const response = await onDelete();
19 | if (response) {
20 | router.refresh();
21 | }
22 | };
23 |
24 | return (
25 |
26 |
Profile Picture
27 |
28 | {userImage ? (
29 | <>
30 |
31 |
32 |
33 |
37 | Remove Logo
38 |
39 | >
40 | ) : (
41 |
42 | )}
43 |
44 |
45 | );
46 | };
47 |
48 | export default ProfilePicture;
49 |
--------------------------------------------------------------------------------
/src/components/global/custom-modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Drawer,
3 | DrawerClose,
4 | DrawerContent,
5 | DrawerDescription,
6 | DrawerFooter,
7 | DrawerHeader,
8 | DrawerTitle,
9 | } from "@/components/ui/drawer";
10 | import { useModal } from "@/providers/modal-provider";
11 |
12 | import React from "react";
13 | import { Button } from "../ui/button";
14 |
15 | type Props = {
16 | title: string;
17 | subheading: string;
18 | children: React.ReactNode;
19 | defaultOpen?: boolean;
20 | };
21 |
22 | const CustomModal = ({ children, subheading, title, defaultOpen }: Props) => {
23 | const { isOpen, setClose } = useModal();
24 | const handleClose = () => setClose();
25 |
26 | return (
27 |
28 |
29 |
30 | {title}
31 |
32 | {subheading}
33 | {children}
34 |
35 |
36 |
37 |
38 |
39 | Close
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default CustomModal;
49 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/billing/page.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 | import { currentUser } from "@clerk/nextjs/server";
3 | import Stripe from "stripe";
4 | import BillingDashboard from "./_components/billing-dashboard";
5 |
6 | type Props = {
7 | searchParams?: { [key: string]: string | undefined };
8 | };
9 |
10 | const Billing = async (props: Props) => {
11 | const { session_id } = props.searchParams ?? {
12 | session_id: "",
13 | };
14 | if (session_id) {
15 | const stripe = new Stripe(process.env.STRIPE_SECRET!, {
16 | typescript: true,
17 | apiVersion: "2023-10-16",
18 | });
19 |
20 | const session = await stripe.checkout.sessions.listLineItems(session_id);
21 | const user = await currentUser();
22 | if (user) {
23 | await db.user.update({
24 | where: {
25 | clerkId: user.id,
26 | },
27 | data: {
28 | tier: session.data[0].description,
29 | credits:
30 | session.data[0].description == "Unlimited"
31 | ? "Unlimited"
32 | : session.data[0].description == "Pro"
33 | ? "100"
34 | : "10",
35 | },
36 | });
37 | }
38 | }
39 |
40 | return (
41 |
42 |
43 | Billing
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default Billing;
51 |
--------------------------------------------------------------------------------
/src/app/api/drive/route.ts:
--------------------------------------------------------------------------------
1 | import { auth, clerkClient } from "@clerk/nextjs/server";
2 | import { google } from "googleapis";
3 | import { NextResponse } from "next/server";
4 |
5 | export async function GET() {
6 | const oauth2Client = new google.auth.OAuth2(
7 | process.env.GOOGLE_CLIENT_ID,
8 | process.env.GOOGLE_CLIENT_SECRET,
9 | process.env.OAUTH2_REDIRECT_URI,
10 | );
11 |
12 | const { userId } = auth();
13 | if (!userId) {
14 | return NextResponse.json({ message: "User not found" });
15 | }
16 |
17 | const clerkResponse = await clerkClient.users.getUserOauthAccessToken(
18 | userId,
19 | "oauth_google",
20 | );
21 |
22 | const accessToken = clerkResponse.data[0].token;
23 | oauth2Client.setCredentials({
24 | access_token: accessToken,
25 | });
26 |
27 | const drive = google.drive({
28 | version: "v3",
29 | auth: oauth2Client,
30 | });
31 |
32 | try {
33 | const response = await drive.files.list();
34 |
35 | if (response) {
36 | return Response.json(
37 | {
38 | message: response.data,
39 | },
40 | {
41 | status: 200,
42 | },
43 | );
44 | } else {
45 | return Response.json(
46 | {
47 | message: "No files found",
48 | },
49 | {
50 | status: 200,
51 | },
52 | );
53 | }
54 | } catch (error) {
55 | return Response.json(
56 | {
57 | message: "Something went wrong",
58 | },
59 | {
60 | status: 500,
61 | },
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/icons/payment.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | type Props = {
4 | selected: boolean;
5 | };
6 |
7 | const Payment = ({ selected }: Props) => {
8 | return (
9 |
16 |
27 |
36 |
45 |
46 | );
47 | };
48 |
49 | export default Payment;
50 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/google-file-details.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardDescription } from "@/components/ui/card";
2 | import { onAddTemplate } from "@/lib/editor-utils";
3 | import { ConnectionProviderProps } from "@/providers/connections-provider";
4 |
5 | type Props = {
6 | nodeConnection: ConnectionProviderProps;
7 | title: string;
8 | gFile: any;
9 | };
10 | const isGoogleFileNotEmpty = (file: any): boolean => {
11 | return Object.keys(file).length > 0 && file.kind !== "";
12 | };
13 |
14 | const GoogleFileDetails = ({ gFile, nodeConnection, title }: Props) => {
15 | if (!isGoogleFileNotEmpty(gFile)) {
16 | return null;
17 | }
18 |
19 | const details = ["kind", "name", "mimeType"];
20 | if (title === "Google Drive") {
21 | details.push("id");
22 | }
23 |
24 | return (
25 |
26 |
27 |
28 | {details.map((detail) => (
29 |
32 | onAddTemplate(nodeConnection, title, gFile[detail])
33 | }
34 | className="flex cursor-pointer gap-2 rounded-full bg-white px-3 py-1 text-gray-500"
35 | >
36 | {detail}:{" "}
37 |
38 | {gFile[detail]}
39 |
40 |
41 | ))}
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default GoogleFileDetails;
49 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/editor-canvas-card-icon-hepler.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { EditorCanvasTypes } from "@/lib/types";
3 | import {
4 | Calendar,
5 | CircuitBoard,
6 | Database,
7 | GitBranch,
8 | HardDrive,
9 | Mail,
10 | MousePointerClickIcon,
11 | Slack,
12 | Timer,
13 | Webhook,
14 | Zap,
15 | } from "lucide-react";
16 |
17 | type Props = { type: EditorCanvasTypes };
18 |
19 | const EditorCanvasIconHelper = ({ type }: Props) => {
20 | switch (type) {
21 | case "Email":
22 | return ;
23 | case "Condition":
24 | return ;
25 | case "AI":
26 | return ;
27 | case "Slack":
28 | return ;
29 | case "Google Drive":
30 | return ;
31 | case "Notion":
32 | return ;
33 | case "Custom Webhook":
34 | return ;
35 | case "Google Calendar":
36 | return ;
37 | case "Trigger":
38 | return ;
39 | case "Action":
40 | return ;
41 | case "Wait":
42 | return ;
43 | default:
44 | return ;
45 | }
46 | };
47 |
48 | export default EditorCanvasIconHelper;
49 |
--------------------------------------------------------------------------------
/src/components/icons/cloud_download.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | type Props = { selected: boolean };
4 |
5 | const Templates = ({ selected }: Props) => {
6 | return (
7 |
14 |
23 |
30 |
31 | );
32 | };
33 |
34 | export default Templates;
35 |
--------------------------------------------------------------------------------
/src/components/icons/category.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | type Props = { selected: boolean };
4 |
5 | function Category({ selected }: Props) {
6 | return (
7 |
14 |
25 |
36 |
47 |
58 |
59 | );
60 | }
61 |
62 | export default Category;
63 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { DM_Sans } from "next/font/google";
3 | import "./globals.css";
4 | import { Toaster } from "@/components/ui/sonner";
5 | import { BillingProvider } from "@/providers/billing-provider";
6 | import ModalProvider from "@/providers/modal-provider";
7 | import { ThemeProvider } from "@/providers/theme-provider";
8 | import { ClerkProvider } from "@clerk/nextjs";
9 |
10 | const font = DM_Sans({ subsets: ["latin"] });
11 |
12 | export const metadata: Metadata = {
13 | title: "Fuzora",
14 | description: "Automate your work with Fuzora",
15 | openGraph: {
16 | title: "Fuzora",
17 | description: "Automate your work with Fuzora",
18 | url: "https://fuzora.xyz",
19 | images: [
20 | {
21 | url: "/fuzora-thumbnails.png",
22 | width: 1260,
23 | height: 800,
24 | },
25 | ],
26 | locale: "en-EN",
27 | },
28 | };
29 |
30 | export default function RootLayout({
31 | children,
32 | }: Readonly<{
33 | children: React.ReactNode;
34 | }>) {
35 | return (
36 |
39 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 | {children}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/api/auth/callback/discord/route.ts:
--------------------------------------------------------------------------------
1 | import url from "url";
2 | import axios from "axios";
3 | import { NextRequest, NextResponse } from "next/server";
4 |
5 | export async function GET(req: NextRequest) {
6 | const code = req.nextUrl.searchParams.get("code");
7 | if (code) {
8 | const data = new url.URLSearchParams();
9 | data.append("client_id", process.env.DISCORD_CLIENT_ID!);
10 | data.append("client_secret", process.env.DISCORD_CLIENT_SECRET!);
11 | data.append("grant_type", "authorization_code");
12 | data.append(
13 | "redirect_uri",
14 | `${process.env.NEXT_PUBLIC_URL}/api/auth/callback/discord`,
15 | );
16 | data.append("code", code.toString());
17 |
18 | const output = await axios.post(
19 | "https://discord.com/api/oauth2/token",
20 | data,
21 | {
22 | headers: {
23 | "Content-Type": "application/x-www-form-urlencoded",
24 | },
25 | },
26 | );
27 |
28 | if (output.data) {
29 | const access = output.data.access_token;
30 | const UserGuilds: any = await axios.get(
31 | `https://discord.com/api/users/@me/guilds`,
32 | {
33 | headers: {
34 | Authorization: `Bearer ${access}`,
35 | },
36 | },
37 | );
38 |
39 | const UserGuild = UserGuilds.data.filter(
40 | (guild: any) => guild.id == output.data.webhook.guild_id,
41 | );
42 |
43 | return NextResponse.redirect(
44 | `${process.env.NEXT_PUBLIC_URL}/connections?webhook_id=${output.data.webhook.id}&webhook_url=${output.data.webhook.url}&webhook_name=${output.data.webhook.name}&guild_id=${output.data.webhook.guild_id}&guild_name=${UserGuild[0].name}&channel_id=${output.data.webhook.channel_id}`,
45 | );
46 | }
47 |
48 | return NextResponse.redirect(`${process.env.NEXT_PUBLIC_URL}/connections`);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/api/auth/callback/notion/route.ts:
--------------------------------------------------------------------------------
1 | import { Client } from "@notionhq/client";
2 | import axios from "axios";
3 | import { NextRequest, NextResponse } from "next/server";
4 |
5 | export async function GET(req: NextRequest) {
6 | const code = req.nextUrl.searchParams.get("code");
7 | const encoded = Buffer.from(
8 | `${process.env.NOTION_CLIENT_ID}:${process.env.NOTION_API_SECRET}`,
9 | ).toString("base64");
10 | if (code) {
11 | const response = await axios("https://api.notion.com/v1/oauth/token", {
12 | method: "POST",
13 | headers: {
14 | "Content-type": "application/json",
15 | Authorization: `Basic ${encoded}`,
16 | "Notion-Version": "2022-06-28",
17 | },
18 | data: JSON.stringify({
19 | grant_type: "authorization_code",
20 | code: code,
21 | redirect_uri: process.env.NOTION_REDIRECT_URI!,
22 | }),
23 | });
24 | if (response) {
25 | const notion = new Client({
26 | auth: response.data.access_token,
27 | });
28 | const databasesPages = await notion.search({
29 | filter: {
30 | value: "database",
31 | property: "object",
32 | },
33 | sort: {
34 | direction: "ascending",
35 | timestamp: "last_edited_time",
36 | },
37 | });
38 | const databaseId = databasesPages?.results?.length
39 | ? databasesPages.results[0].id
40 | : "";
41 |
42 | console.log(databaseId);
43 |
44 | return NextResponse.redirect(
45 | `${process.env.NEXT_PUBLIC_URL}/connections?access_token=${response.data.access_token}&workspace_name=${response.data.workspace_name}&workspace_icon=${response.data.workspace_icon}&workspace_id=${response.data.workspace_id}&database_id=${databaseId}`,
46 | );
47 | }
48 | }
49 |
50 | return NextResponse.redirect(`${process.env.NEXT_PUBLIC_URL}/connections`);
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | *,
6 | *::before,
7 | *::after {
8 | box-sizing: border-box;
9 | }
10 |
11 | *::-webkit-scrollbar {
12 | display: none !important;
13 | }
14 | .bg-radial-gradient {
15 | background-image: radial-gradient(
16 | circle at 10% 20%,
17 | rgba(4, 159, 108, 1) 0%,
18 | rgba(194, 254, 113, 1) 90.1%
19 | );
20 | }
21 |
22 | @layer base {
23 | :root {
24 | --background: 0 0% 100%;
25 | --foreground: 0 0% 3.9%;
26 | --card: 0 0% 100%;
27 | --card-foreground: 0 0% 3.9%;
28 | --popover: 0 0% 100%;
29 | --popover-foreground: 0 0% 3.9%;
30 | --primary: 0 0% 9%;
31 | --primary-foreground: 0 0% 98%;
32 | --secondary: 0 0% 96.1%;
33 | --secondary-foreground: 0 0% 9%;
34 | --muted: 0 0% 96.1%;
35 | --muted-foreground: 0 0% 45.1%;
36 | --accent: 0 0% 96.1%;
37 | --accent-foreground: 0 0% 9%;
38 | --destructive: 0 84.2% 60.2%;
39 | --destructive-foreground: 0 0% 98%;
40 | --border: 0 0% 89.8%;
41 | --input: 0 0% 89.8%;
42 | --ring: 0 0% 3.9%;
43 | --radius: 0.5rem;
44 | }
45 |
46 | .dark {
47 | --background: 0 0% 3.9%;
48 | --foreground: 0 0% 98%;
49 | --card: 0 0% 3.9%;
50 | --card-foreground: 0 0% 98%;
51 | --popover: 0 0% 3.9%;
52 | --popover-foreground: 0 0% 98%;
53 | --primary: 0 0% 98%;
54 | --primary-foreground: 0 0% 9%;
55 | --secondary: 0 0% 14.9%;
56 | --secondary-foreground: 0 0% 98%;
57 | --muted: 0 0% 14.9%;
58 | --muted-foreground: 0 0% 63.9%;
59 | --accent: 0 0% 14.9%;
60 | --accent-foreground: 0 0% 98%;
61 | --destructive: 0 62.8% 30.6%;
62 | --destructive-foreground: 0 0% 98%;
63 | --border: 0 0% 14.9%;
64 | --input: 0 0% 14.9%;
65 | --ring: 0 0% 83.1%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/ui/resizable.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { GripVertical } from "lucide-react";
4 | import * as ResizablePrimitive from "react-resizable-panels";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const ResizablePanelGroup = ({
9 | className,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
19 | );
20 |
21 | const ResizablePanel = ResizablePrimitive.Panel;
22 |
23 | const ResizableHandle = ({
24 | withHandle,
25 | className,
26 | ...props
27 | }: React.ComponentProps & {
28 | withHandle?: boolean;
29 | }) => (
30 | div]:rotate-90",
33 | className,
34 | )}
35 | {...props}
36 | >
37 | {withHandle && (
38 |
39 |
40 |
41 | )}
42 |
43 | );
44 |
45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
46 |
--------------------------------------------------------------------------------
/src/components/icons/clipboard.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | const Logs = ({ selected }: { selected: boolean }) => {
4 | return (
5 |
12 |
23 |
30 |
39 |
48 |
49 | );
50 | };
51 |
52 | export default Logs;
53 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/connections/_components/connection-card.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardDescription,
4 | CardHeader,
5 | CardTitle,
6 | } from "@/components/ui/card";
7 | import { ConnectionTypes } from "@/lib/types";
8 | import Image from "next/image";
9 | import Link from "next/link";
10 |
11 | type Props = {
12 | type: ConnectionTypes;
13 | icon: string;
14 | title: ConnectionTypes;
15 | description: string;
16 | callback?: () => void;
17 | connected: {} & any;
18 | };
19 |
20 | const ConnectionCard = ({
21 | description,
22 | type,
23 | icon,
24 | title,
25 | connected,
26 | }: Props) => {
27 | return (
28 |
29 |
30 |
31 |
38 |
39 |
40 | {title}
41 | {description}
42 |
43 |
44 |
45 | {connected[type] ? (
46 |
47 | Connected
48 |
49 | ) : (
50 |
62 | Connect
63 |
64 | )}
65 |
66 |
67 | );
68 | };
69 |
70 | export default ConnectionCard;
71 |
--------------------------------------------------------------------------------
/src/providers/modal-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { createContext, useContext, useEffect, useState } from "react";
3 |
4 | interface ModalProviderProps {
5 | children: React.ReactNode;
6 | }
7 |
8 | export type ModalData = {};
9 |
10 | type ModalContextType = {
11 | data: ModalData;
12 | isOpen: boolean;
13 | setOpen: (modal: React.ReactNode, fetchData?: () => Promise) => void;
14 | setClose: () => void;
15 | };
16 |
17 | export const ModalContext = createContext({
18 | data: {},
19 | isOpen: false,
20 | setOpen: (modal: React.ReactNode, fetchData?: () => Promise) => {},
21 | setClose: () => {},
22 | });
23 |
24 | const ModalProvider: React.FC = ({ children }) => {
25 | const [isOpen, setIsOpen] = useState(false);
26 | const [data, setData] = useState({});
27 | const [showingModal, setShowingModal] = useState(null);
28 | const [isMounted, setIsMounted] = useState(false);
29 |
30 | useEffect(() => {
31 | setIsMounted(true);
32 | }, []);
33 |
34 | const setOpen = async (
35 | modal: React.ReactNode,
36 | fetchData?: () => Promise,
37 | ) => {
38 | if (modal) {
39 | if (fetchData) {
40 | const fetchedData = await fetchData();
41 | setData({ ...data, ...(fetchedData || {}) });
42 | }
43 | setShowingModal(modal);
44 | setIsOpen(true);
45 | }
46 | };
47 |
48 | const setClose = () => {
49 | setIsOpen(false);
50 | setData({});
51 | };
52 |
53 | if (!isMounted) return null;
54 |
55 | return (
56 |
57 | {children}
58 | {showingModal}
59 |
60 | );
61 | };
62 |
63 | export const useModal = () => {
64 | const context = useContext(ModalContext);
65 | if (!context) {
66 | throw new Error("useModal must be used within the modal provider");
67 | }
68 | return context;
69 | };
70 |
71 | export default ModalProvider;
72 |
--------------------------------------------------------------------------------
/src/app/api/auth/callback/slack/route.tsx:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 |
3 | export async function GET(req: NextRequest) {
4 | // Extract the code parameter from the query string
5 | const code = req.nextUrl.searchParams.get("code");
6 |
7 | // Check if the code parameter is missing
8 | if (!code) {
9 | return new NextResponse("Code not provided", { status: 400 });
10 | }
11 |
12 | try {
13 | // Make a POST request to Slack's OAuth endpoint to exchange the code for an access token
14 | const response = await fetch("https://slack.com/api/oauth.v2.access", {
15 | method: "POST",
16 | headers: {
17 | "Content-Type": "application/x-www-form-urlencoded",
18 | },
19 | body: new URLSearchParams({
20 | code,
21 | client_id: process.env.SLACK_CLIENT_ID!,
22 | client_secret: process.env.SLACK_CLIENT_SECRET!,
23 | redirect_uri: process.env.SLACK_REDIRECT_URI!,
24 | }),
25 | });
26 |
27 | const data = await response.json();
28 |
29 | // Check if the response indicates a failure
30 | if (!data.ok) {
31 | throw new Error(data.error || "Slack OAuth failed");
32 | }
33 |
34 | if (!!data?.ok) {
35 | const appId = data?.app_id;
36 | const userId = data?.authed_user?.id;
37 | const userToken = data?.authed_user?.access_token;
38 | const accessToken = data?.access_token;
39 | const botUserId = data?.bot_user_id;
40 | const teamId = data?.team?.id;
41 | const teamName = data?.team?.name;
42 |
43 | // Handle the successful OAuth flow and redirect the user
44 | return NextResponse.redirect(
45 | `${process.env.NEXT_PUBLIC_URL}/connections?app_id=${appId}&authed_user_id=${userId}&authed_user_token=${userToken}&slack_access_token=${accessToken}&bot_user_id=${botUserId}&team_id=${teamId}&team_name=${teamName}`,
46 | );
47 | }
48 | } catch (error) {
49 | console.error(error);
50 | return new NextResponse("Internal Server Error", { status: 500 });
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot";
2 | import { type VariantProps, cva } from "class-variance-authority";
3 | import * as React from "react";
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 |
--------------------------------------------------------------------------------
/src/app/api/drive-activity/route.ts:
--------------------------------------------------------------------------------
1 | import { db } from "@/lib/db";
2 | import { auth, clerkClient } from "@clerk/nextjs/server";
3 | import { google } from "googleapis";
4 | import { NextResponse } from "next/server";
5 | import { v4 as uuidv4 } from "uuid";
6 |
7 | export async function GET() {
8 | const oauth2Client = new google.auth.OAuth2(
9 | process.env.GOOGLE_CLIENT_ID,
10 | process.env.GOOGLE_CLIENT_SECRET,
11 | process.env.OAUTH2_REDIRECT_URI,
12 | );
13 |
14 | const { userId } = auth();
15 | if (!userId) {
16 | return NextResponse.json({ message: "User not found" });
17 | }
18 |
19 | const clerkResponse = await clerkClient.users.getUserOauthAccessToken(
20 | userId,
21 | "oauth_google",
22 | );
23 |
24 | const accessToken = clerkResponse.data[0].token;
25 | oauth2Client.setCredentials({
26 | access_token: accessToken,
27 | });
28 |
29 | const drive = google.drive({
30 | version: "v3",
31 | auth: oauth2Client,
32 | });
33 |
34 | const channelId = uuidv4();
35 |
36 | const startPageTokenRes = await drive.changes.getStartPageToken({});
37 | const startPageToken = startPageTokenRes.data.startPageToken;
38 | if (startPageToken == null) {
39 | throw new Error("startPageToken is unexpectedly null");
40 | }
41 |
42 | const listener = await drive.changes.watch({
43 | pageToken: startPageToken,
44 | supportsAllDrives: true,
45 | supportsTeamDrives: true,
46 | requestBody: {
47 | id: channelId,
48 | type: "web_hook",
49 | address: `${process.env.NGROK_URI}/api/drive-activity/notification`,
50 | kind: "api#channel",
51 | },
52 | });
53 |
54 | if (listener.status == 200) {
55 | //if listener created store its channel id in db
56 | const channelStored = await db.user.updateMany({
57 | where: {
58 | clerkId: userId,
59 | },
60 | data: {
61 | googleResourceId: listener.data.resourceId,
62 | },
63 | });
64 |
65 | if (channelStored) {
66 | return new NextResponse("Listening to changes...");
67 | }
68 | }
69 |
70 | return new NextResponse("Oops! something went wrong, try again");
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as TabsPrimitive from "@radix-ui/react-tabs";
4 | import * as React from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Tabs = TabsPrimitive.Root;
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ));
23 | TabsList.displayName = TabsPrimitive.List.displayName;
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ));
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ));
53 | TabsContent.displayName = TabsPrimitive.Content.displayName;
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent };
56 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
2 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
3 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
4 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
5 |
6 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
7 | CLERK_SECRET_KEY=
8 |
9 | DATABASE_URL=
10 |
11 | ## Development URL
12 | NEXT_PUBLIC_URL=https://localhost:3000
13 | NEXT_PUBLIC_DOMAIN=localhost:3000
14 | NEXT_PUBLIC_SCHEME=https://
15 |
16 | NEXT_PUBLIC_GOOGLE_SCOPES=https://www.googleapis.com/auth/drive
17 | NEXT_PUBLIC_OAUTH2_ENDPOINT=https://accounts.google.com/o/oauth2/v2/auth
18 |
19 | NEXT_PUBLIC_UPLOAD_CARE_CSS_SRC=https://cdn.jsdelivr.net/npm/@uploadcare/blocks@
20 | NEXT_PUBLIC_UPLOAD_CARE_SRC_PACKAGE=/web/lr-file-uploader-regular.min.css
21 |
22 | DISCORD_CLIENT_ID=
23 | DISCORD_CLIENT_SECRET=
24 | DISCORD_TOKEN=
25 | DISCORD_PUBLICK_KEY=
26 | NEXT_PUBLIC_DISCORD_REDIRECT=https://discord.com/oauth2/authorize?client_id=*CLIENTID*&response_type=code&redirect_uri=https%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fcallback%2Fdiscord&scope=identify+guilds+connections+guilds.members.read+email+webhook.incoming
27 |
28 | NOTION_API_SECRET=
29 | NOTION_CLIENT_ID=
30 | NOTION_REDIRECT_URI=https://localhost:3000/api/auth/callback/notion
31 | NEXT_PUBLIC_NOTION_AUTH_URL=https://api.notion.com/v1/oauth/authorize?client_id=*CLIENTID*&response_type=code&owner=user&redirect_uri=https%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fcallback%2Fnotion
32 |
33 | # ,groups:read,mpim:read,im:read'
34 |
35 | SLACK_SIGNING_SECRET=
36 | SLACK_BOT_TOKEN=
37 | SLACK_APP_TOKEN=
38 | SLACK_CLIENT_ID=
39 | SLACK_CLIENT_SECRET=
40 | SLACK_REDIRECT_URI=https://localhost:3000/api/auth/callback/slack
41 | NEXT_PUBLIC_SLACK_REDIRECT=https://slack.com/oauth/v2/authorize?client_id=*CLIENTID*&scope=chat:write,channels:read,groups:read,mpim:read,im:read&user_scope=chat:write,channels:read,groups:read,mpim:read,im:read&redirect_uri=https%3A%2F%2Flocalhost%3A3000%2Fapi%2Fauth%2Fcallback%2Fslack
42 |
43 | GOOGLE_CLIENT_ID=
44 | GOOGLE_CLIENT_SECRET=
45 | OAUTH2_REDIRECT_URI=https://electric-grizzly-7.clerk.accounts.dev/v1/oauth_callback
46 | NGROK_URI=
47 | CRON_JOB_KEY=
48 | STRIPE_SECRET=
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import ProfileForm from "@/components/forms/profile-form";
2 | import { db } from "@/lib/db";
3 | import { currentUser } from "@clerk/nextjs/server";
4 | import ProfilePicture from "./_components/profile-picture";
5 |
6 | type Props = {};
7 |
8 | const Settings = async (props: Props) => {
9 | const authUser = await currentUser();
10 | if (!authUser) return null;
11 |
12 | const user = await db.user.findUnique({ where: { clerkId: authUser.id } });
13 | const removeProfileImage = async () => {
14 | "use server";
15 | const response = await db.user.update({
16 | where: {
17 | clerkId: authUser.id,
18 | },
19 | data: {
20 | profileImage: "",
21 | },
22 | });
23 | return response;
24 | };
25 |
26 | const uploadProfileImage = async (image: string) => {
27 | "use server";
28 | const id = authUser.id;
29 | const response = await db.user.update({
30 | where: {
31 | clerkId: id,
32 | },
33 | data: {
34 | profileImage: image,
35 | },
36 | });
37 |
38 | return response;
39 | };
40 |
41 | const updateUserInfo = async (name: string) => {
42 | "use server";
43 |
44 | const updateUser = await db.user.update({
45 | where: {
46 | clerkId: authUser.id,
47 | },
48 | data: {
49 | name,
50 | },
51 | });
52 | return updateUser;
53 | };
54 |
55 | return (
56 |
57 |
58 | Settings
59 |
60 |
61 |
62 |
User Profile
63 |
64 | Add or update your information
65 |
66 |
67 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default Settings;
79 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/flow-instance.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Button } from "@/components/ui/button";
3 | import { useNodeConnections } from "@/providers/connections-provider";
4 | import { usePathname } from "next/navigation";
5 | import React, { useCallback, useEffect, useState } from "react";
6 | import { toast } from "sonner";
7 | import {
8 | onCreateNodesEdges,
9 | onFlowPublish,
10 | } from "../_actions/workflow-connections";
11 |
12 | type Props = {
13 | children: React.ReactNode;
14 | edges: any[];
15 | nodes: any[];
16 | };
17 |
18 | const FlowInstance = ({ children, edges, nodes }: Props) => {
19 | const pathname = usePathname();
20 | const [isFlow, setIsFlow] = useState([]);
21 | const { nodeConnection } = useNodeConnections();
22 |
23 | const onFlowAutomation = useCallback(async () => {
24 | const flow = await onCreateNodesEdges(
25 | pathname.split("/").pop()!,
26 | JSON.stringify(nodes),
27 | JSON.stringify(edges),
28 | JSON.stringify(isFlow),
29 | );
30 |
31 | if (flow) toast.message(flow.message);
32 | }, [nodeConnection]);
33 |
34 | const onPublishWorkflow = useCallback(async () => {
35 | const response = await onFlowPublish(pathname.split("/").pop()!, true);
36 | if (response) toast.message(response);
37 | }, []);
38 |
39 | const onAutomateFlow = async () => {
40 | const flows: any = [];
41 | const connectedEdges = edges.map((edge) => edge.target);
42 | connectedEdges.map((target) => {
43 | nodes.map((node) => {
44 | if (node.id === target) {
45 | flows.push(node.type);
46 | }
47 | });
48 | });
49 |
50 | setIsFlow(flows);
51 | };
52 |
53 | useEffect(() => {
54 | onAutomateFlow();
55 | }, [edges]);
56 |
57 | return (
58 |
59 |
60 |
61 | Save
62 |
63 |
64 | Publish
65 |
66 |
67 | {children}
68 |
69 | );
70 | };
71 |
72 | export default FlowInstance;
73 |
--------------------------------------------------------------------------------
/src/components/global/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { UserButton } from "@clerk/nextjs";
2 | import { currentUser } from "@clerk/nextjs/server";
3 | import { MenuIcon } from "lucide-react";
4 | import Link from "next/link";
5 |
6 | type Props = {};
7 |
8 | const Navbar = async (props: Props) => {
9 | const user = await currentUser();
10 | return (
11 |
12 |
15 |
16 |
17 |
18 | Products
19 |
20 |
21 | Pricing
22 |
23 |
24 | Clients
25 |
26 |
27 | Resources
28 |
29 |
30 | Documentation
31 |
32 |
33 | Enterprise
34 |
35 |
36 |
37 |
38 |
42 |
43 |
44 | {user ? "Dashboard" : "Get Started"}
45 |
46 |
47 | {user ? : null}
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default Navbar;
55 |
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { ConnectionProviderProps } from "@/providers/connections-provider";
2 | import { z } from "zod";
3 |
4 | export const EditUserProfileSchema = z.object({
5 | email: z.string().email("Required"),
6 | name: z.string().min(1, "Required"),
7 | });
8 |
9 | export const WorkflowFormSchema = z.object({
10 | name: z.string().min(1, "Required"),
11 | description: z.string().min(1, "Required"),
12 | });
13 |
14 | export type ConnectionTypes = "Google Drive" | "Notion" | "Slack" | "Discord";
15 |
16 | export type Connection = {
17 | title: ConnectionTypes;
18 | description: string;
19 | image: string;
20 | connectionKey: keyof ConnectionProviderProps;
21 | accessTokenKey?: string;
22 | alwaysTrue?: boolean;
23 | slackSpecial?: boolean;
24 | };
25 |
26 | export type EditorCanvasTypes =
27 | | "Email"
28 | | "Condition"
29 | | "AI"
30 | | "Slack"
31 | | "Google Drive"
32 | | "Notion"
33 | | "Custom Webhook"
34 | | "Google Calendar"
35 | | "Trigger"
36 | | "Action"
37 | | "Wait";
38 |
39 | export type EditorCanvasCardType = {
40 | title: string;
41 | description: string;
42 | completed: boolean;
43 | current: boolean;
44 | metadata: any;
45 | type: EditorCanvasTypes;
46 | };
47 |
48 | export type EditorNodeType = {
49 | id: string;
50 | type: EditorCanvasCardType["type"];
51 | position: {
52 | x: number;
53 | y: number;
54 | };
55 | data: EditorCanvasCardType;
56 | };
57 |
58 | export type EditorNode = EditorNodeType;
59 |
60 | export type EditorActions =
61 | | {
62 | type: "LOAD_DATA";
63 | payload: {
64 | elements: EditorNode[];
65 | edges: {
66 | id: string;
67 | source: string;
68 | target: string;
69 | }[];
70 | };
71 | }
72 | | {
73 | type: "UPDATE_NODE";
74 | payload: {
75 | elements: EditorNode[];
76 | };
77 | }
78 | | { type: "REDO" }
79 | | { type: "UNDO" }
80 | | {
81 | type: "SELECTED_ELEMENT";
82 | payload: {
83 | element: EditorNode;
84 | };
85 | };
86 |
87 | export const nodeMapper: Record = {
88 | Notion: "notionNode",
89 | Slack: "slackNode",
90 | Discord: "discordNode",
91 | "Google Drive": "googleNode",
92 | };
93 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as AccordionPrimitive from "@radix-ui/react-accordion";
4 | import { ChevronDown } from "lucide-react";
5 | import * as React from "react";
6 |
7 | import { cn } from "@/lib/utils";
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = "AccordionItem";
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className,
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
59 |
--------------------------------------------------------------------------------
/src/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 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/src/components/infobar/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Input } from "@/components/ui/input";
3 | import { Book, Headphones, Search } from "lucide-react";
4 | import { useEffect } from "react";
5 |
6 | import { onPaymentDetails } from "@/app/(main)/(pages)/billing/_actions/payment-connecetions";
7 | import {
8 | Tooltip,
9 | TooltipContent,
10 | TooltipProvider,
11 | TooltipTrigger,
12 | } from "@/components/ui/tooltip";
13 | import { useBilling } from "@/providers/billing-provider";
14 | import { UserButton } from "@clerk/nextjs";
15 |
16 | type Props = {};
17 |
18 | const InfoBar = (props: Props) => {
19 | const { credits, tier, setCredits, setTier } = useBilling();
20 |
21 | const onGetPayment = async () => {
22 | const response = await onPaymentDetails();
23 | if (response) {
24 | setTier(response.tier!);
25 | setCredits(response.credits!);
26 | }
27 | };
28 |
29 | useEffect(() => {
30 | onGetPayment();
31 | }, []);
32 |
33 | return (
34 |
35 |
36 | Credits
37 | {tier == "Unlimited" ? (
38 | Unlimited
39 | ) : (
40 |
41 | {credits}/{tier == "Free" ? "10" : tier == "Pro" && "100"}
42 |
43 | )}
44 |
45 |
46 |
47 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | Contact Support
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Guide
69 |
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default InfoBar;
78 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/_components/workflow.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import {
3 | Card,
4 | CardDescription,
5 | CardHeader,
6 | CardTitle,
7 | } from "@/components/ui/card";
8 | import { Label } from "@/components/ui/label";
9 | import { Switch } from "@/components/ui/switch";
10 | import Image from "next/image";
11 | import Link from "next/link";
12 | import { useState } from "react";
13 | import { toast } from "sonner";
14 | import { onFlowPublish } from "../_actions/workflow-connections";
15 | type Props = {
16 | name: string;
17 | description: string;
18 | id: string;
19 | };
20 |
21 | const Workflow = ({ description, id, name }: Props) => {
22 | const [publish, setPublish] = useState(false);
23 | const onPublishFlow = async (event: any) => {
24 | const response = await onFlowPublish(
25 | id,
26 | event.target.ariaChecked === "false",
27 | );
28 | if (response) {
29 | setPublish((publish) => !publish);
30 | toast.message(response);
31 | }
32 | };
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
46 |
53 |
60 |
61 |
62 | {name}
63 | {description}
64 |
65 |
66 |
67 |
68 |
69 | {publish ? "On" : "Off"}
70 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default Workflow;
78 |
--------------------------------------------------------------------------------
/src/app/api/clerk-webhook/route.ts:
--------------------------------------------------------------------------------
1 | import { Webhook } from 'svix'
2 | import { headers } from 'next/headers'
3 | import { WebhookEvent } from '@clerk/nextjs/server'
4 | import { db } from "@/lib/db";
5 | import { NextResponse } from "next/server";
6 |
7 | export async function POST(req: Request) {
8 | const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET
9 |
10 | if (!WEBHOOK_SECRET) {
11 | throw new Error('Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local')
12 | }
13 |
14 | // Get the headers
15 | const headerPayload = headers();
16 | const svix_id = headerPayload.get("svix-id");
17 | const svix_timestamp = headerPayload.get("svix-timestamp");
18 | const svix_signature = headerPayload.get("svix-signature");
19 |
20 | // If there are no headers, error out
21 | if (!svix_id || !svix_timestamp || !svix_signature) {
22 | return new Response('Error occured -- no svix headers', {
23 | status: 400
24 | })
25 | }
26 |
27 | // Get the body
28 | const payload = await req.json()
29 | const body = JSON.stringify(payload);
30 |
31 | // Create a new Svix instance with your secret.
32 | const wh = new Webhook(WEBHOOK_SECRET);
33 |
34 | let evt: WebhookEvent
35 |
36 | // Verify the payload with the headers
37 | try {
38 | evt = wh.verify(body, {
39 | "svix-id": svix_id,
40 | "svix-timestamp": svix_timestamp,
41 | "svix-signature": svix_signature,
42 | }) as WebhookEvent
43 | } catch (err) {
44 | console.error('Error verifying webhook:', err);
45 | return new Response('Error occured', {
46 | status: 400
47 | })
48 | }
49 |
50 | // Handle the webhook
51 | const { id, email_addresses, first_name, image_url }: any = evt.data;
52 | const email = email_addresses[0]?.email_address;
53 |
54 | try {
55 | await db.user.upsert({
56 | where: { clerkId: id },
57 | update: {
58 | email,
59 | name: first_name,
60 | profileImage: image_url,
61 | },
62 | create: {
63 | clerkId: id,
64 | email,
65 | name: first_name || "",
66 | profileImage: image_url || "",
67 | },
68 | });
69 | return new NextResponse("User updated in database successfully", { status: 200 });
70 | } catch (error) {
71 | console.error('Error updating user in database:', error);
72 | return new NextResponse("Error updating user in database", { status: 500 });
73 | }
74 | }
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/billing/_components/subscription-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | type Props = {
4 | onPayment(id: string): void;
5 | products: any[];
6 | tier: string;
7 | };
8 |
9 | import { Button } from "@/components/ui/button";
10 | import {
11 | Card,
12 | CardContent,
13 | CardDescription,
14 | CardHeader,
15 | CardTitle,
16 | } from "@/components/ui/card";
17 |
18 | export const SubscriptionCard = ({ onPayment, products, tier }: Props) => {
19 | console.log(products);
20 | return (
21 |
22 | {products &&
23 | products.map((product: any) => (
24 |
25 |
26 | {product.nickname}
27 |
28 |
29 |
30 | {product.nickname == "Unlimited"
31 | ? "Enjoy a monthly torrent of credits flooding your account, empowering you to tackle even the most ambitious automation tasks effortlessly."
32 | : product.nickname == "Pro"
33 | ? "Experience a monthly surge of credits to supercharge your automation efforts. Ideal for small to medium-sized projects seeking consistent support."
34 | : product.nickname == "Free" &&
35 | "Get a monthly wave of credits to automate your tasks with ease. Perfect for starters looking to dip their toes into Fuzora's automation capabilities."}
36 |
37 |
38 |
39 | {product.nickname == "Free"
40 | ? "10"
41 | : product.nickname == "Pro"
42 | ? "100"
43 | : product.nickname == "Unlimited" && "unlimited"}{" "}
44 | credits
45 |
46 |
47 | {product.nickname == "Free"
48 | ? "Free"
49 | : product.nickname == "Pro"
50 | ? "29.99"
51 | : product.nickname === "Unlimited" && "99.99"}
52 | /mo
53 |
54 |
55 | {product.nickname == tier ? (
56 |
57 | Active
58 |
59 | ) : (
60 | onPayment(product.id)} variant="outline">
61 | Purchase
62 |
63 | )}
64 |
65 |
66 | ))}
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/connections/_actions/notion-connection.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/db";
4 | import { currentUser } from "@clerk/nextjs/server";
5 | import { Client } from "@notionhq/client";
6 |
7 | export const onNotionConnect = async (
8 | access_token: string,
9 | workspace_id: string,
10 | workspace_icon: string,
11 | workspace_name: string,
12 | database_id: string,
13 | id: string,
14 | ) => {
15 | "use server";
16 | if (access_token) {
17 | //check if notion is connected
18 | const notion_connected = await db.notion.findFirst({
19 | where: {
20 | accessToken: access_token,
21 | },
22 | include: {
23 | connections: {
24 | select: {
25 | type: true,
26 | },
27 | },
28 | },
29 | });
30 |
31 | if (!notion_connected) {
32 | //create connection
33 | await db.notion.create({
34 | data: {
35 | userId: id,
36 | workspaceIcon: workspace_icon!,
37 | accessToken: access_token,
38 | workspaceId: workspace_id!,
39 | workspaceName: workspace_name!,
40 | databaseId: database_id,
41 | connections: {
42 | create: {
43 | userId: id,
44 | type: "Notion",
45 | },
46 | },
47 | },
48 | });
49 | }
50 | }
51 | };
52 | export const getNotionConnection = async () => {
53 | const user = await currentUser();
54 | if (user) {
55 | const connection = await db.notion.findFirst({
56 | where: {
57 | userId: user.id,
58 | },
59 | });
60 | if (connection) {
61 | return connection;
62 | }
63 | }
64 | };
65 |
66 | export const getNotionDatabase = async (
67 | databaseId: string,
68 | accessToken: string,
69 | ) => {
70 | const notion = new Client({
71 | auth: accessToken,
72 | });
73 | const response = await notion.databases.retrieve({ database_id: databaseId });
74 | return response;
75 | };
76 |
77 | export const onCreateNewPageInDatabase = async (
78 | databaseId: string,
79 | accessToken: string,
80 | content: string,
81 | ) => {
82 | const notion = new Client({
83 | auth: accessToken,
84 | });
85 |
86 | console.log(databaseId);
87 | const response = await notion.pages.create({
88 | parent: {
89 | type: "database_id",
90 | database_id: databaseId,
91 | },
92 | properties: {
93 | name: [
94 | {
95 | text: {
96 | content: content,
97 | },
98 | },
99 | ],
100 | },
101 | });
102 | if (response) {
103 | return response;
104 | }
105 | };
106 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/editor-canvas-card-single.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import { EditorCanvasCardType } from "@/lib/types";
3 | import { useEditor } from "@/providers/editor-provider";
4 | import { useMemo } from "react";
5 | import { Position, useNodeId } from "reactflow";
6 | import CustomHandle from "./custom-handle";
7 | import EditorCanvasIconHelper from "./editor-canvas-card-icon-hepler";
8 |
9 | import {
10 | Card,
11 | CardDescription,
12 | CardHeader,
13 | CardTitle,
14 | } from "@/components/ui/card";
15 | import clsx from "clsx";
16 |
17 | type Props = {};
18 |
19 | const EditorCanvasCardSingle = ({ data }: { data: EditorCanvasCardType }) => {
20 | const { dispatch, state } = useEditor();
21 | const nodeId = useNodeId();
22 | const logo = useMemo(() => {
23 | return ;
24 | }, [data]);
25 |
26 | return (
27 | <>
28 | {data.type !== "Trigger" && data.type !== "Google Drive" && (
29 |
34 | )}
35 | {
37 | e.stopPropagation();
38 | const val = state.editor.elements.find((n) => n.id === nodeId);
39 | if (val)
40 | dispatch({
41 | type: "SELECTED_ELEMENT",
42 | payload: {
43 | element: val,
44 | },
45 | });
46 | }}
47 | className="relative max-w-[400px] dark:border-muted-foreground/70"
48 | >
49 |
50 | {logo}
51 |
52 |
{data.title}
53 |
54 |
55 | ID:
56 | {nodeId}
57 |
58 | {data.description}
59 |
60 |
61 |
62 |
63 | {data.type}
64 |
65 | = 0.6 && Math.random() < 0.8,
69 | "bg-red-500": Math.random() >= 0.8,
70 | })}
71 | >
72 |
73 |
74 | >
75 | );
76 | };
77 |
78 | export default EditorCanvasCardSingle;
79 |
--------------------------------------------------------------------------------
/src/components/icons/settings.tsx:
--------------------------------------------------------------------------------
1 | import clsx from "clsx";
2 |
3 | type Props = { selected: boolean };
4 |
5 | const Settings = ({ selected }: Props) => {
6 | return (
7 |
14 |
21 |
30 |
31 | );
32 | };
33 |
34 | export default Settings;
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fuzora",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --experimental-https",
7 | "build": "prisma generate && next build",
8 | "start": "next start",
9 | "lint": "biome check src",
10 | "format": "biome check --write src",
11 | "clean": "rm -rf .next .turbo .vercel node_modules",
12 | "prepare": "husky"
13 | },
14 | "dependencies": {
15 | "@clerk/nextjs": "^5.7.2",
16 | "@commitlint/cli": "^19.5.0",
17 | "@commitlint/config-conventional": "^19.5.0",
18 | "@hookform/resolvers": "^3.9.0",
19 | "@neondatabase/serverless": "^0.10.1",
20 | "@notionhq/client": "^2.2.15",
21 | "@prisma/adapter-neon": "^5.20.0",
22 | "@prisma/client": "^5.20.0",
23 | "@radix-ui/react-accordion": "^1.2.1",
24 | "@radix-ui/react-dialog": "^1.1.2",
25 | "@radix-ui/react-dropdown-menu": "^2.1.2",
26 | "@radix-ui/react-label": "^2.1.0",
27 | "@radix-ui/react-popover": "^1.1.2",
28 | "@radix-ui/react-progress": "^1.1.0",
29 | "@radix-ui/react-separator": "^1.1.0",
30 | "@radix-ui/react-slot": "^1.1.0",
31 | "@radix-ui/react-switch": "^1.1.1",
32 | "@radix-ui/react-tabs": "^1.1.1",
33 | "@radix-ui/react-tooltip": "^1.1.3",
34 | "@tsparticles/engine": "^3.5.0",
35 | "@tsparticles/react": "^3.0.0",
36 | "@tsparticles/slim": "^3.5.0",
37 | "@types/uuid": "^9.0.8",
38 | "@types/ws": "^8.5.12",
39 | "@uploadcare/blocks": "^0.35.2",
40 | "axios": "^1.7.7",
41 | "class-variance-authority": "^0.7.0",
42 | "clsx": "^2.1.1",
43 | "cmdk": "0.2.0",
44 | "dotenv": "^16.4.5",
45 | "framer-motion": "^11.11.8",
46 | "googleapis": "^134.0.0",
47 | "lint-staged": "^15.2.10",
48 | "lucide-react": "^0.358.0",
49 | "next": "14.2.14",
50 | "next-themes": "^0.3.0",
51 | "react": "^18.3.1",
52 | "react-dom": "^18.3.1",
53 | "react-hook-form": "^7.53.0",
54 | "react-resizable-panels": "^2.1.4",
55 | "reactflow": "^11.11.4",
56 | "sonner": "^1.5.0",
57 | "stripe": "^14.25.0",
58 | "svix": "^1.37.0",
59 | "tailwind-merge": "^2.5.3",
60 | "tailwindcss-animate": "^1.0.7",
61 | "uuid": "^9.0.1",
62 | "vaul": "^0.9.9",
63 | "ws": "^8.18.0",
64 | "zod": "^3.23.8",
65 | "zustand": "^4.5.5"
66 | },
67 | "devDependencies": {
68 | "@biomejs/biome": "1.9.3",
69 | "@types/node": "^20.16.11",
70 | "@types/react": "^18.3.11",
71 | "@types/react-dom": "^18.3.1",
72 | "autoprefixer": "^10.4.20",
73 | "bufferutil": "^4.0.8",
74 | "husky": "^9.1.6",
75 | "postcss": "^8.4.47",
76 | "prisma": "^5.20.0",
77 | "tailwindcss": "^3.4.13",
78 | "typescript": "^5.6.3"
79 | },
80 | "lint-staged": {
81 | "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
82 | "biome check --write --no-errors-on-unmatched"
83 | ]
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/global/container-scroll-animation.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { motion, useScroll, useTransform } from "framer-motion";
3 | import Image from "next/image";
4 | import React, { useRef } from "react";
5 |
6 | export const ContainerScroll = ({
7 | titleComponent,
8 | }: {
9 | titleComponent: string | React.ReactNode;
10 | }) => {
11 | const containerRef = useRef(null);
12 | const { scrollYProgress } = useScroll({
13 | target: containerRef,
14 | });
15 | const [isMobile, setIsMobile] = React.useState(false);
16 |
17 | React.useEffect(() => {
18 | const checkMobile = () => {
19 | setIsMobile(window.innerWidth <= 768);
20 | };
21 | checkMobile();
22 | window.addEventListener("resize", checkMobile);
23 | return () => {
24 | window.removeEventListener("resize", checkMobile);
25 | };
26 | }, []);
27 |
28 | const scaleDimensions = () => {
29 | return isMobile ? [0.7, 0.9] : [1.05, 1];
30 | };
31 |
32 | const rotate = useTransform(scrollYProgress, [0, 1], [20, 0]);
33 | const scale = useTransform(scrollYProgress, [0, 1], scaleDimensions());
34 | const translate = useTransform(scrollYProgress, [0, 1], [0, -100]);
35 |
36 | return (
37 |
51 | );
52 | };
53 |
54 | export const Header = ({ translate, titleComponent }: any) => {
55 | return (
56 |
62 | {titleComponent}
63 |
64 | );
65 | };
66 |
67 | export const Card = ({
68 | rotate,
69 | scale,
70 | translate,
71 | }: {
72 | rotate: any;
73 | scale: any;
74 | translate: any;
75 | }) => {
76 | return (
77 |
86 |
87 |
93 |
94 |
95 | );
96 | };
97 |
--------------------------------------------------------------------------------
/src/components/forms/profile-form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { EditUserProfileSchema } from "@/lib/types";
4 | import { zodResolver } from "@hookform/resolvers/zod";
5 | import { Loader2 } from "lucide-react";
6 | import { useEffect, useState } from "react";
7 | import { useForm } from "react-hook-form";
8 | import { z } from "zod";
9 | import { Button } from "../ui/button";
10 | import {
11 | Form,
12 | FormControl,
13 | FormField,
14 | FormItem,
15 | FormLabel,
16 | FormMessage,
17 | } from "../ui/form";
18 | import { Input } from "../ui/input";
19 |
20 | type Props = {
21 | user: any;
22 | onUpdate?: any;
23 | };
24 |
25 | const ProfileForm = ({ user, onUpdate }: Props) => {
26 | const [isLoading, setIsLoading] = useState(false);
27 | const form = useForm>({
28 | mode: "onChange",
29 | resolver: zodResolver(EditUserProfileSchema),
30 | defaultValues: {
31 | name: user.name,
32 | email: user.email,
33 | },
34 | });
35 |
36 | const handleSubmit = async (
37 | values: z.infer,
38 | ) => {
39 | setIsLoading(true);
40 | await onUpdate(values.name);
41 | setIsLoading(false);
42 | };
43 |
44 | useEffect(() => {
45 | form.reset({ name: user.name, email: user.email });
46 | }, [user]);
47 |
48 | return (
49 |
100 |
101 | );
102 | };
103 |
104 | export default ProfileForm;
105 |
--------------------------------------------------------------------------------
/src/components/global/infinite-moving-cards.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import Image from "next/image";
5 | import React, { useEffect, useState } from "react";
6 |
7 | export const InfiniteMovingCards = ({
8 | items,
9 | direction = "left",
10 | speed = "fast",
11 | pauseOnHover = true,
12 | className,
13 | }: {
14 | items: {
15 | href: string;
16 | }[];
17 | direction?: "left" | "right";
18 | speed?: "fast" | "normal" | "slow";
19 | pauseOnHover?: boolean;
20 | className?: string;
21 | }) => {
22 | const containerRef = React.useRef(null);
23 | const scrollerRef = React.useRef(null);
24 |
25 | useEffect(() => {
26 | addAnimation();
27 | }, []);
28 |
29 | const [start, setStart] = useState(false);
30 | function addAnimation() {
31 | if (containerRef.current && scrollerRef.current) {
32 | const scrollerContent = Array.from(scrollerRef.current.children);
33 |
34 | scrollerContent.forEach((item) => {
35 | const duplicatedItem = item.cloneNode(true);
36 | if (scrollerRef.current) {
37 | scrollerRef.current.appendChild(duplicatedItem);
38 | }
39 | });
40 |
41 | getDirection();
42 | getSpeed();
43 | setStart(true);
44 | }
45 | }
46 | const getDirection = () => {
47 | if (containerRef.current) {
48 | if (direction === "left") {
49 | containerRef.current.style.setProperty(
50 | "--animation-direction",
51 | "forwards",
52 | );
53 | } else {
54 | containerRef.current.style.setProperty(
55 | "--animation-direction",
56 | "reverse",
57 | );
58 | }
59 | }
60 | };
61 | const getSpeed = () => {
62 | if (containerRef.current) {
63 | if (speed === "fast") {
64 | containerRef.current.style.setProperty("--animation-duration", "20s");
65 | } else if (speed === "normal") {
66 | containerRef.current.style.setProperty("--animation-duration", "40s");
67 | } else {
68 | containerRef.current.style.setProperty("--animation-duration", "80s");
69 | }
70 | }
71 | };
72 | console.log(items);
73 | return (
74 |
81 |
89 | {items.map((item, idx) => (
90 |
98 | ))}
99 |
100 |
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/connections/_actions/discord-connection.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { db } from "@/lib/db";
4 | import { currentUser } from "@clerk/nextjs/server";
5 | import axios from "axios";
6 |
7 | export const onDiscordConnect = async (
8 | channel_id: string,
9 | webhook_id: string,
10 | webhook_name: string,
11 | webhook_url: string,
12 | id: string,
13 | guild_name: string,
14 | guild_id: string,
15 | ) => {
16 | //check if webhook id params set
17 | if (webhook_id) {
18 | //check if webhook exists in database with userid
19 | const webhook = await db.discordWebhook.findFirst({
20 | where: {
21 | userId: id,
22 | },
23 | include: {
24 | connections: {
25 | select: {
26 | type: true,
27 | },
28 | },
29 | },
30 | });
31 |
32 | //if webhook does not exist for this user
33 | if (!webhook) {
34 | //create new webhook
35 | await db.discordWebhook.create({
36 | data: {
37 | userId: id,
38 | webhookId: webhook_id,
39 | channelId: channel_id!,
40 | guildId: guild_id!,
41 | name: webhook_name!,
42 | url: webhook_url!,
43 | guildName: guild_name!,
44 | connections: {
45 | create: {
46 | userId: id,
47 | type: "Discord",
48 | },
49 | },
50 | },
51 | });
52 | }
53 |
54 | //if webhook exists return check for duplicate
55 | if (webhook) {
56 | //check if webhook exists for target channel id
57 | const webhook_channel = await db.discordWebhook.findUnique({
58 | where: {
59 | channelId: channel_id,
60 | },
61 | include: {
62 | connections: {
63 | select: {
64 | type: true,
65 | },
66 | },
67 | },
68 | });
69 |
70 | //if no webhook for channel create new webhook
71 | if (!webhook_channel) {
72 | await db.discordWebhook.create({
73 | data: {
74 | userId: id,
75 | webhookId: webhook_id,
76 | channelId: channel_id!,
77 | guildId: guild_id!,
78 | name: webhook_name!,
79 | url: webhook_url!,
80 | guildName: guild_name!,
81 | connections: {
82 | create: {
83 | userId: id,
84 | type: "Discord",
85 | },
86 | },
87 | },
88 | });
89 | }
90 | }
91 | }
92 | };
93 |
94 | export const getDiscordConnectionUrl = async () => {
95 | const user = await currentUser();
96 | if (user) {
97 | const webhook = await db.discordWebhook.findFirst({
98 | where: {
99 | userId: user.id,
100 | },
101 | select: {
102 | url: true,
103 | name: true,
104 | guildName: true,
105 | },
106 | });
107 |
108 | return webhook;
109 | }
110 | };
111 |
112 | export const postContentToWebHook = async (content: string, url: string) => {
113 | console.log(content);
114 | if (content != "") {
115 | const posted = await axios.post(url, { content });
116 | if (posted) {
117 | return { message: "success" };
118 | }
119 | return { message: "failed request" };
120 | }
121 | return { message: "String empty" };
122 | };
123 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/render-connection-accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import ConnectionCard from "@/app/(main)/(pages)/connections/_components/connection-card";
3 | import { AccordionContent } from "@/components/ui/accordion";
4 | import MultipleSelector from "@/components/ui/multiple-selector";
5 | import { Connection } from "@/lib/types";
6 | import { useNodeConnections } from "@/providers/connections-provider";
7 | import { EditorState } from "@/providers/editor-provider";
8 | import { useFuzzieStore } from "@/store";
9 | import React from "react";
10 |
11 | const frameworks = [
12 | {
13 | value: "next.js",
14 | label: "Next.js",
15 | },
16 | {
17 | value: "sveltekit",
18 | label: "SvelteKit",
19 | },
20 | {
21 | value: "nuxt.js",
22 | label: "Nuxt.js",
23 | },
24 | {
25 | value: "remix",
26 | label: "Remix",
27 | },
28 | {
29 | value: "astro",
30 | label: "Astro",
31 | },
32 | ];
33 |
34 | const RenderConnectionAccordion = ({
35 | connection,
36 | state,
37 | }: {
38 | connection: Connection;
39 | state: EditorState;
40 | }) => {
41 | const {
42 | title,
43 | image,
44 | description,
45 | connectionKey,
46 | accessTokenKey,
47 | alwaysTrue,
48 | slackSpecial,
49 | } = connection;
50 |
51 | const { nodeConnection } = useNodeConnections();
52 | const { slackChannels, selectedSlackChannels, setSelectedSlackChannels } =
53 | useFuzzieStore();
54 |
55 | const [open, setOpen] = React.useState(false);
56 | const [value, setValue] = React.useState("");
57 |
58 | const connectionData = (nodeConnection as any)[connectionKey];
59 |
60 | const isConnected =
61 | alwaysTrue ||
62 | (nodeConnection[connectionKey] &&
63 | accessTokenKey &&
64 | connectionData[accessTokenKey!]);
65 |
66 | return (
67 |
68 | {state.editor.selectedNode.data.title === title && (
69 | <>
70 |
77 | {slackSpecial && isConnected && (
78 |
79 | {slackChannels?.length ? (
80 | <>
81 |
82 | Select the slack channels to send notification and messages:
83 |
84 |
91 | no results found.
92 |
93 | }
94 | />
95 | >
96 | ) : (
97 | "No Slack channels found. Please add your Slack bot to your Slack channel"
98 | )}
99 |
100 | )}
101 | >
102 | )}
103 |
104 | );
105 | };
106 |
107 | export default RenderConnectionAccordion;
108 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/billing/_components/billing-dashboard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useBilling } from "@/providers/billing-provider";
4 | import axios from "axios";
5 | import { useEffect, useState } from "react";
6 | import CreditTracker from "./creadits-tracker";
7 | import { SubscriptionCard } from "./subscription-card";
8 |
9 | type Props = {};
10 |
11 | const BillingDashboard = (props: Props) => {
12 | const { credits, tier } = useBilling();
13 | const [stripeProducts, setStripeProducts] = useState([]);
14 | const [loading, setLoading] = useState(false);
15 |
16 | const onStripeProducts = async () => {
17 | setLoading(true);
18 | const { data } = await axios.get("/api/payment");
19 | if (data) {
20 | setStripeProducts(data);
21 | setLoading(false);
22 | }
23 | };
24 |
25 | useEffect(() => {
26 | onStripeProducts();
27 | }, []);
28 |
29 | const onPayment = async (id: string) => {
30 | const { data } = await axios.post(
31 | "/api/payment",
32 | {
33 | priceId: id,
34 | },
35 | {
36 | headers: {
37 | "Content-Type": "application/json",
38 | },
39 | },
40 | );
41 | window.location.assign(data);
42 | };
43 |
44 | return (
45 | <>
46 | {/* {loading ? (
47 |
65 | ) : ( */}
66 | <>
67 |
68 |
73 |
74 |
75 | >
76 | {/* )} */}
77 | >
78 | );
79 | };
80 |
81 | export default BillingDashboard;
82 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/google-drive-files.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { CardContainer } from "@/components/global/3d-card";
3 | import { Button } from "@/components/ui/button";
4 | import { Card, CardDescription } from "@/components/ui/card";
5 | import axios from "axios";
6 | import { useEffect, useState } from "react";
7 | import { toast } from "sonner";
8 | import { getGoogleListener } from "../../../_actions/workflow-connections";
9 |
10 | type Props = {};
11 |
12 | const GoogleDriveFiles = (props: Props) => {
13 | const [loading, setLoading] = useState(false);
14 | const [isListening, setIsListening] = useState(false);
15 |
16 | const reqGoogle = async () => {
17 | setLoading(true);
18 | const response = await axios.get("/api/drive-activity");
19 | if (response) {
20 | toast.message(response.data);
21 | setLoading(false);
22 | setIsListening(true);
23 | }
24 | setIsListening(false);
25 | };
26 |
27 | const onListener = async () => {
28 | const listener = await getGoogleListener();
29 | if (listener?.googleResourceId !== null) {
30 | setIsListening(true);
31 | }
32 | };
33 |
34 | useEffect(() => {
35 | onListener();
36 | }, []);
37 |
38 | return (
39 |
40 | {isListening ? (
41 |
42 |
43 | Listening...
44 |
45 |
46 | ) : (
47 |
53 | {loading ? (
54 |
72 | ) : (
73 | "Create Listener"
74 | )}
75 |
76 | )}
77 |
78 | );
79 | };
80 |
81 | export default GoogleDriveFiles;
82 |
--------------------------------------------------------------------------------
/src/components/forms/workflow-form.tsx:
--------------------------------------------------------------------------------
1 | import { onCreateWorkflow } from "@/app/(main)/(pages)/workflows/_actions/workflow-connections";
2 | import { WorkflowFormSchema } from "@/lib/types";
3 | import { useModal } from "@/providers/modal-provider";
4 | import { zodResolver } from "@hookform/resolvers/zod";
5 | import { Loader2 } from "lucide-react";
6 | import { useRouter } from "next/navigation";
7 | import { useForm } from "react-hook-form";
8 | import { toast } from "sonner";
9 | import { z } from "zod";
10 | import { Button } from "../ui/button";
11 | import {
12 | Card,
13 | CardContent,
14 | CardDescription,
15 | CardHeader,
16 | CardTitle,
17 | } from "../ui/card";
18 | import {
19 | Form,
20 | FormControl,
21 | FormField,
22 | FormItem,
23 | FormLabel,
24 | FormMessage,
25 | } from "../ui/form";
26 | import { Input } from "../ui/input";
27 |
28 | type Props = {
29 | title?: string;
30 | subTitle?: string;
31 | };
32 |
33 | const Workflowform = ({ subTitle, title }: Props) => {
34 | const { setClose } = useModal();
35 | const form = useForm>({
36 | mode: "onChange",
37 | resolver: zodResolver(WorkflowFormSchema),
38 | defaultValues: {
39 | name: "",
40 | description: "",
41 | },
42 | });
43 |
44 | const isLoading = form.formState.isLoading;
45 | const router = useRouter();
46 |
47 | const handleSubmit = async (values: z.infer) => {
48 | const workflow = await onCreateWorkflow(values.name, values.description);
49 | if (workflow) {
50 | toast.message(workflow.message);
51 | router.refresh();
52 | }
53 | setClose();
54 | };
55 |
56 | return (
57 |
58 | {title && subTitle && (
59 |
60 | {title}
61 | {subTitle}
62 |
63 | )}
64 |
65 |
108 |
109 |
110 |
111 | );
112 | };
113 |
114 | export default Workflowform;
115 |
--------------------------------------------------------------------------------
/src/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Drawer as DrawerPrimitive } from "vaul";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | );
17 | Drawer.displayName = "Drawer";
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger;
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal;
22 |
23 | const DrawerClose = DrawerPrimitive.Close;
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ));
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ));
56 | DrawerContent.displayName = "DrawerContent";
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | );
67 | DrawerHeader.displayName = "DrawerHeader";
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | );
78 | DrawerFooter.displayName = "DrawerFooter";
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ));
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ));
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | };
119 |
--------------------------------------------------------------------------------
/src/providers/connections-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { createContext, useContext, useState } from "react";
3 |
4 | export type ConnectionProviderProps = {
5 | discordNode: {
6 | webhookURL: string;
7 | content: string;
8 | webhookName: string;
9 | guildName: string;
10 | };
11 | setDiscordNode: React.Dispatch>;
12 | googleNode: {}[];
13 | setGoogleNode: React.Dispatch>;
14 | notionNode: {
15 | accessToken: string;
16 | databaseId: string;
17 | workspaceName: string;
18 | content: "";
19 | };
20 | workflowTemplate: {
21 | discord?: string;
22 | notion?: string;
23 | slack?: string;
24 | };
25 | setNotionNode: React.Dispatch>;
26 | slackNode: {
27 | appId: string;
28 | authedUserId: string;
29 | authedUserToken: string;
30 | slackAccessToken: string;
31 | botUserId: string;
32 | teamId: string;
33 | teamName: string;
34 | content: string;
35 | };
36 | setSlackNode: React.Dispatch>;
37 | setWorkFlowTemplate: React.Dispatch<
38 | React.SetStateAction<{
39 | discord?: string;
40 | notion?: string;
41 | slack?: string;
42 | }>
43 | >;
44 | isLoading: boolean;
45 | setIsLoading: React.Dispatch>;
46 | };
47 |
48 | type ConnectionWithChildProps = {
49 | children: React.ReactNode;
50 | };
51 |
52 | const InitialValues: ConnectionProviderProps = {
53 | discordNode: {
54 | webhookURL: "",
55 | content: "",
56 | webhookName: "",
57 | guildName: "",
58 | },
59 | googleNode: [],
60 | notionNode: {
61 | accessToken: "",
62 | databaseId: "",
63 | workspaceName: "",
64 | content: "",
65 | },
66 | workflowTemplate: {
67 | discord: "",
68 | notion: "",
69 | slack: "",
70 | },
71 | slackNode: {
72 | appId: "",
73 | authedUserId: "",
74 | authedUserToken: "",
75 | slackAccessToken: "",
76 | botUserId: "",
77 | teamId: "",
78 | teamName: "",
79 | content: "",
80 | },
81 | isLoading: false,
82 | setGoogleNode: () => undefined,
83 | setDiscordNode: () => undefined,
84 | setNotionNode: () => undefined,
85 | setSlackNode: () => undefined,
86 | setIsLoading: () => undefined,
87 | setWorkFlowTemplate: () => undefined,
88 | };
89 |
90 | const ConnectionsContext = createContext(InitialValues);
91 | const { Provider } = ConnectionsContext;
92 |
93 | export const ConnectionsProvider = ({ children }: ConnectionWithChildProps) => {
94 | const [discordNode, setDiscordNode] = useState(InitialValues.discordNode);
95 | const [googleNode, setGoogleNode] = useState(InitialValues.googleNode);
96 | const [notionNode, setNotionNode] = useState(InitialValues.notionNode);
97 | const [slackNode, setSlackNode] = useState(InitialValues.slackNode);
98 | const [isLoading, setIsLoading] = useState(InitialValues.isLoading);
99 | const [workflowTemplate, setWorkFlowTemplate] = useState(
100 | InitialValues.workflowTemplate,
101 | );
102 |
103 | const values = {
104 | discordNode,
105 | setDiscordNode,
106 | googleNode,
107 | setGoogleNode,
108 | notionNode,
109 | setNotionNode,
110 | slackNode,
111 | setSlackNode,
112 | isLoading,
113 | setIsLoading,
114 | workflowTemplate,
115 | setWorkFlowTemplate,
116 | };
117 |
118 | return {children} ;
119 | };
120 |
121 | export const useNodeConnections = () => {
122 | const nodeConnection = useContext(ConnectionsContext);
123 | return { nodeConnection };
124 | };
125 |
--------------------------------------------------------------------------------
/src/components/sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Separator } from "@/components/ui/separator";
3 | import {
4 | Tooltip,
5 | TooltipContent,
6 | TooltipProvider,
7 | TooltipTrigger,
8 | } from "@/components/ui/tooltip";
9 | import { menuOptions } from "@/lib/constant";
10 | import clsx from "clsx";
11 | import { Database, GitBranch, LucideMousePointerClick } from "lucide-react";
12 | import Link from "next/link";
13 | import { usePathname } from "next/navigation";
14 | import { ModeToggle } from "../global/mode-toggle";
15 |
16 | type Props = {};
17 |
18 | const MenuOptions = (props: Props) => {
19 | const pathName = usePathname();
20 |
21 | return (
22 |
23 |
24 |
25 | fuzora.
26 |
27 |
28 | {menuOptions.map((menuItem) => (
29 |
30 |
31 |
32 |
33 |
43 |
46 |
47 |
48 |
49 |
53 | {menuItem.name}
54 |
55 |
56 |
57 | ))}
58 |
59 |
60 |
61 |
65 |
69 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default MenuOptions;
86 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/connections/page.tsx:
--------------------------------------------------------------------------------
1 | import { CONNECTIONS } from "@/lib/constant";
2 | import { currentUser } from "@clerk/nextjs/server";
3 | import { onDiscordConnect } from "./_actions/discord-connection";
4 | import { getUserData } from "./_actions/get-user";
5 | import { onNotionConnect } from "./_actions/notion-connection";
6 | import { onSlackConnect } from "./_actions/slack-connection";
7 | import ConnectionCard from "./_components/connection-card";
8 |
9 | type Props = {
10 | searchParams?: { [key: string]: string | undefined };
11 | };
12 |
13 | const Connections = async (props: Props) => {
14 | const {
15 | webhook_id,
16 | webhook_name,
17 | webhook_url,
18 | guild_id,
19 | guild_name,
20 | channel_id,
21 | access_token,
22 | workspace_name,
23 | workspace_icon,
24 | workspace_id,
25 | database_id,
26 | app_id,
27 | authed_user_id,
28 | authed_user_token,
29 | slack_access_token,
30 | bot_user_id,
31 | team_id,
32 | team_name,
33 | } = props.searchParams ?? {
34 | webhook_id: "",
35 | webhook_name: "",
36 | webhook_url: "",
37 | guild_id: "",
38 | guild_name: "",
39 | channel_id: "",
40 | access_token: "",
41 | workspace_name: "",
42 | workspace_icon: "",
43 | workspace_id: "",
44 | database_id: "",
45 | app_id: "",
46 | authed_user_id: "",
47 | authed_user_token: "",
48 | slack_access_token: "",
49 | bot_user_id: "",
50 | team_id: "",
51 | team_name: "",
52 | };
53 |
54 | const user = await currentUser();
55 | if (!user) return null;
56 |
57 | const onUserConnections = async () => {
58 | console.log(database_id);
59 | await onDiscordConnect(
60 | channel_id!,
61 | webhook_id!,
62 | webhook_name!,
63 | webhook_url!,
64 | user.id,
65 | guild_name!,
66 | guild_id!,
67 | );
68 | await onNotionConnect(
69 | access_token!,
70 | workspace_id!,
71 | workspace_icon!,
72 | workspace_name!,
73 | database_id!,
74 | user.id,
75 | );
76 |
77 | await onSlackConnect(
78 | app_id!,
79 | authed_user_id!,
80 | authed_user_token!,
81 | slack_access_token!,
82 | bot_user_id!,
83 | team_id!,
84 | team_name!,
85 | user.id,
86 | );
87 |
88 | const connections: any = {};
89 |
90 | const user_info = await getUserData(user.id);
91 |
92 | //get user info with all connections
93 | user_info?.connections.map((connection) => {
94 | connections[connection.type] = true;
95 | return (connections[connection.type] = true);
96 | });
97 |
98 | // Google Drive connection will always be true
99 | // as it is given access during the login process
100 | return { ...connections, "Google Drive": true };
101 | };
102 |
103 | const connections = await onUserConnections();
104 |
105 | return (
106 |
107 |
108 | Connections
109 |
110 |
111 |
112 | Connect all your apps directly from here. You may need to connect
113 | these apps regularly to refresh verification
114 | {CONNECTIONS.map((connection) => (
115 |
123 | ))}
124 |
125 |
126 |
127 | );
128 | };
129 |
130 | export default Connections;
131 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/connections/_actions/slack-connection.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { Option } from "@/components/ui/multiple-selector";
4 | import { db } from "@/lib/db";
5 | import { currentUser } from "@clerk/nextjs/server";
6 | import axios from "axios";
7 |
8 | export const onSlackConnect = async (
9 | app_id: string,
10 | authed_user_id: string,
11 | authed_user_token: string,
12 | slack_access_token: string,
13 | bot_user_id: string,
14 | team_id: string,
15 | team_name: string,
16 | user_id: string,
17 | ): Promise => {
18 | if (!slack_access_token) return;
19 |
20 | const slackConnection = await db.slack.findFirst({
21 | where: { slackAccessToken: slack_access_token },
22 | include: { connections: true },
23 | });
24 |
25 | if (!slackConnection) {
26 | await db.slack.create({
27 | data: {
28 | userId: user_id,
29 | appId: app_id,
30 | authedUserId: authed_user_id,
31 | authedUserToken: authed_user_token,
32 | slackAccessToken: slack_access_token,
33 | botUserId: bot_user_id,
34 | teamId: team_id,
35 | teamName: team_name,
36 | connections: {
37 | create: { userId: user_id, type: "Slack" },
38 | },
39 | },
40 | });
41 | }
42 | };
43 |
44 | export const getSlackConnection = async () => {
45 | const user = await currentUser();
46 | if (user) {
47 | return await db.slack.findFirst({
48 | where: { userId: user.id },
49 | });
50 | }
51 | return null;
52 | };
53 |
54 | export async function listBotChannels(
55 | slackAccessToken: string,
56 | ): Promise {
57 | const url = `https://slack.com/api/conversations.list?${new URLSearchParams({
58 | types: "public_channel,private_channel",
59 | limit: "200",
60 | })}`;
61 |
62 | try {
63 | const { data } = await axios.get(url, {
64 | headers: { Authorization: `Bearer ${slackAccessToken}` },
65 | });
66 |
67 | console.log(data);
68 |
69 | if (!data.ok) throw new Error(data.error);
70 |
71 | if (!data?.channels?.length) return [];
72 |
73 | return data.channels
74 | .filter((ch: any) => ch.is_member)
75 | .map((ch: any) => {
76 | return { label: ch.name, value: ch.id };
77 | });
78 | } catch (error: any) {
79 | console.error("Error listing bot channels:", error.message);
80 | throw error;
81 | }
82 | }
83 |
84 | const postMessageInSlackChannel = async (
85 | slackAccessToken: string,
86 | slackChannel: string,
87 | content: string,
88 | ): Promise => {
89 | try {
90 | await axios.post(
91 | "https://slack.com/api/chat.postMessage",
92 | { channel: slackChannel, text: content },
93 | {
94 | headers: {
95 | Authorization: `Bearer ${slackAccessToken}`,
96 | "Content-Type": "application/json;charset=utf-8",
97 | },
98 | },
99 | );
100 | console.log(`Message posted successfully to channel ID: ${slackChannel}`);
101 | } catch (error: any) {
102 | console.error(
103 | `Error posting message to Slack channel ${slackChannel}:`,
104 | error?.response?.data || error.message,
105 | );
106 | }
107 | };
108 |
109 | // Wrapper function to post messages to multiple Slack channels
110 | export const postMessageToSlack = async (
111 | slackAccessToken: string,
112 | selectedSlackChannels: Option[],
113 | content: string,
114 | ): Promise<{ message: string }> => {
115 | if (!content) return { message: "Content is empty" };
116 | if (!selectedSlackChannels?.length)
117 | return { message: "Channel not selected" };
118 |
119 | try {
120 | selectedSlackChannels
121 | .map((channel) => channel?.value)
122 | .forEach((channel) => {
123 | postMessageInSlackChannel(slackAccessToken, channel, content);
124 | });
125 | } catch (error) {
126 | return { message: "Message could not be sent to Slack" };
127 | }
128 |
129 | return { message: "Success" };
130 | };
131 |
--------------------------------------------------------------------------------
/src/providers/editor-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { EditorActions, EditorNodeType } from "@/lib/types";
4 | import { Dispatch, createContext, useContext, useReducer } from "react";
5 |
6 | export type EditorNode = EditorNodeType;
7 |
8 | export type Editor = {
9 | elements: EditorNode[];
10 | edges: {
11 | id: string;
12 | source: string;
13 | target: string;
14 | }[];
15 | selectedNode: EditorNodeType;
16 | };
17 |
18 | export type HistoryState = {
19 | history: Editor[];
20 | currentIndex: number;
21 | };
22 |
23 | export type EditorState = {
24 | editor: Editor;
25 | history: HistoryState;
26 | };
27 |
28 | const initialEditorState: EditorState["editor"] = {
29 | elements: [],
30 | selectedNode: {
31 | data: {
32 | completed: false,
33 | current: false,
34 | description: "",
35 | metadata: {},
36 | title: "",
37 | type: "Trigger",
38 | },
39 | id: "",
40 | position: { x: 0, y: 0 },
41 | type: "Trigger",
42 | },
43 | edges: [],
44 | };
45 |
46 | const initialHistoryState: HistoryState = {
47 | history: [initialEditorState],
48 | currentIndex: 0,
49 | };
50 |
51 | const initialState: EditorState = {
52 | editor: initialEditorState,
53 | history: initialHistoryState,
54 | };
55 |
56 | const editorReducer = (
57 | state: EditorState = initialState,
58 | action: EditorActions,
59 | ): EditorState => {
60 | switch (action.type) {
61 | case "REDO":
62 | if (state.history.currentIndex < state.history.history.length - 1) {
63 | const nextIndex = state.history.currentIndex + 1;
64 | const nextEditorState = { ...state.history.history[nextIndex] };
65 | const redoState = {
66 | ...state,
67 | editor: nextEditorState,
68 | history: {
69 | ...state.history,
70 | currentIndex: nextIndex,
71 | },
72 | };
73 | return redoState;
74 | }
75 | return state;
76 |
77 | case "UNDO":
78 | if (state.history.currentIndex > 0) {
79 | const prevIndex = state.history.currentIndex - 1;
80 | const prevEditorState = { ...state.history.history[prevIndex] };
81 | const undoState = {
82 | ...state,
83 | editor: prevEditorState,
84 | history: {
85 | ...state.history,
86 | currentIndex: prevIndex,
87 | },
88 | };
89 | return undoState;
90 | }
91 | return state;
92 |
93 | case "LOAD_DATA":
94 | return {
95 | ...state,
96 | editor: {
97 | ...state.editor,
98 | elements: action.payload.elements || initialEditorState.elements,
99 | edges: action.payload.edges,
100 | },
101 | };
102 | case "SELECTED_ELEMENT":
103 | return {
104 | ...state,
105 | editor: {
106 | ...state.editor,
107 | selectedNode: action.payload.element,
108 | },
109 | };
110 | default:
111 | return state;
112 | }
113 | };
114 |
115 | export type EditorContextData = {
116 | previewMode: boolean;
117 | setPreviewMode: (previewMode: boolean) => void;
118 | };
119 |
120 | export const EditorContext = createContext<{
121 | state: EditorState;
122 | dispatch: Dispatch;
123 | }>({
124 | state: initialState,
125 | dispatch: () => undefined,
126 | });
127 |
128 | type EditorProps = {
129 | children: React.ReactNode;
130 | };
131 |
132 | const EditorProvider = (props: EditorProps) => {
133 | const [state, dispatch] = useReducer(editorReducer, initialState);
134 |
135 | return (
136 |
142 | {props.children}
143 |
144 | );
145 | };
146 |
147 | export const useEditor = () => {
148 | const context = useContext(EditorContext);
149 | if (!context) {
150 | throw new Error("useEditor Hook must be used within the editor Provider");
151 | }
152 | return context;
153 | };
154 |
155 | export default EditorProvider;
156 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 |
2 | generator client {
3 | provider = "prisma-client-js"
4 | previewFeatures = ["driverAdapters"]
5 | }
6 |
7 | datasource db {
8 | provider = "postgresql"
9 | url = env("DATABASE_URL")
10 | }
11 |
12 | model User {
13 | id Int @id @default(autoincrement())
14 |
15 | clerkId String @unique
16 | name String?
17 | email String @unique
18 | profileImage String?
19 | tier String? @default("Free")
20 | credits String? @default("10")
21 |
22 | createdAt DateTime @default(now())
23 | updatedAt DateTime @updatedAt
24 | localGoogleId String? @unique
25 | googleResourceId String? @unique
26 |
27 | LocalGoogleCredential LocalGoogleCredential?
28 | DiscordWebhook DiscordWebhook[]
29 | Notion Notion[]
30 | Slack Slack[]
31 | connections Connections[]
32 | workflows Workflows[]
33 | }
34 |
35 | model LocalGoogleCredential {
36 | id String @id @default(uuid())
37 | accessToken String @unique
38 |
39 | folderId String?
40 | pageToken String?
41 | channelId String @unique @default(uuid())
42 | subscribed Boolean @default(false)
43 |
44 | createdAt DateTime @default(now())
45 | updatedAt DateTime @updatedAt
46 |
47 | userId Int @unique
48 | user User @relation(fields: [userId], references: [id])
49 | }
50 |
51 | model DiscordWebhook {
52 | id String @id @default(uuid())
53 | webhookId String @unique
54 | url String @unique
55 | name String
56 | guildName String
57 | guildId String
58 | channelId String @unique
59 | user User @relation(fields: [userId], references: [clerkId])
60 | userId String
61 | connections Connections[]
62 | }
63 |
64 | model Slack {
65 | id String @id @default(uuid())
66 |
67 | appId String
68 | authedUserId String
69 | authedUserToken String @unique
70 | slackAccessToken String @unique
71 | botUserId String
72 | teamId String
73 | teamName String
74 |
75 | User User @relation(fields: [userId], references: [clerkId])
76 | userId String
77 | connections Connections[]
78 | }
79 |
80 | model Notion {
81 | id String @id @default(uuid())
82 | accessToken String @unique
83 | workspaceId String @unique
84 | databaseId String @unique
85 | workspaceName String
86 | workspaceIcon String
87 | User User @relation(fields: [userId], references: [clerkId])
88 | userId String
89 | connections Connections[]
90 | }
91 |
92 | model Connections {
93 | id String @id @default(uuid())
94 | type String @unique
95 | DiscordWebhook DiscordWebhook? @relation(fields: [discordWebhookId], references: [id])
96 | discordWebhookId String?
97 | Notion Notion? @relation(fields: [notionId], references: [id])
98 | notionId String?
99 | User User? @relation(fields: [userId], references: [clerkId])
100 | userId String?
101 | Slack Slack? @relation(fields: [slackId], references: [id])
102 | slackId String?
103 | }
104 |
105 | model Workflows {
106 | id String @id @default(uuid())
107 | nodes String?
108 | edges String?
109 | name String
110 | discordTemplate String?
111 | notionTemplate String?
112 | slackTemplate String?
113 | slackChannels String[]
114 | slackAccessToken String?
115 | notionAccessToken String?
116 | notionDbId String?
117 | flowPath String?
118 | cronPath String?
119 | publish Boolean? @default(false)
120 | description String
121 | User User @relation(fields: [userId], references: [clerkId])
122 | userId String
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as DialogPrimitive from "@radix-ui/react-dialog";
4 | import { X } from "lucide-react";
5 | import * as React from "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 |
--------------------------------------------------------------------------------
/src/components/global/3d-card.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { cn } from "@/lib/utils";
4 | import React, {
5 | createContext,
6 | useState,
7 | useContext,
8 | useRef,
9 | useEffect,
10 | } from "react";
11 |
12 | const MouseEnterContext = createContext<
13 | [boolean, React.Dispatch>] | undefined
14 | >(undefined);
15 |
16 | export const CardContainer = ({
17 | children,
18 | className,
19 | containerClassName,
20 | }: {
21 | children?: React.ReactNode;
22 | className?: string;
23 | containerClassName?: string;
24 | }) => {
25 | const containerRef = useRef(null);
26 | const [isMouseEntered, setIsMouseEntered] = useState(false);
27 |
28 | const handleMouseMove = (e: React.MouseEvent) => {
29 | if (!containerRef.current) return;
30 | const { left, top, width, height } =
31 | containerRef.current.getBoundingClientRect();
32 | const x = (e.clientX - left - width / 2) / 25;
33 | const y = (e.clientY - top - height / 2) / 25;
34 | containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`;
35 | };
36 |
37 | const handleMouseEnter = (e: React.MouseEvent) => {
38 | setIsMouseEntered(true);
39 | if (!containerRef.current) return;
40 | };
41 |
42 | const handleMouseLeave = (e: React.MouseEvent) => {
43 | if (!containerRef.current) return;
44 | setIsMouseEntered(false);
45 | containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`;
46 | };
47 | return (
48 |
49 |
55 |
68 | {children}
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export const CardBody = ({
76 | children,
77 | className,
78 | }: {
79 | children: React.ReactNode;
80 | className?: string;
81 | }) => {
82 | return (
83 | *]:[transform-style:preserve-3d]",
86 | className,
87 | )}
88 | >
89 | {children}
90 |
91 | );
92 | };
93 |
94 | export const CardItem = ({
95 | as: Tag = "div",
96 | children,
97 | className,
98 | translateX = 0,
99 | translateY = 0,
100 | translateZ = 0,
101 | rotateX = 0,
102 | rotateY = 0,
103 | rotateZ = 0,
104 | ...rest
105 | }: {
106 | as?: React.ElementType;
107 | children: React.ReactNode;
108 | className?: string;
109 | translateX?: number | string;
110 | translateY?: number | string;
111 | translateZ?: number | string;
112 | rotateX?: number | string;
113 | rotateY?: number | string;
114 | rotateZ?: number | string;
115 | }) => {
116 | const ref = useRef(null);
117 | const [isMouseEntered] = useMouseEnter();
118 |
119 | useEffect(() => {
120 | handleAnimations();
121 | }, [isMouseEntered]);
122 |
123 | const handleAnimations = () => {
124 | if (!ref.current) return;
125 | if (isMouseEntered) {
126 | ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`;
127 | } else {
128 | ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`;
129 | }
130 | };
131 |
132 | return (
133 |
138 | {children}
139 |
140 | );
141 | };
142 |
143 | // Create a hook to use the context
144 | export const useMouseEnter = () => {
145 | const context = useContext(MouseEnterContext);
146 | if (context === undefined) {
147 | throw new Error("useMouseEnter must be used within a MouseEnterProvider");
148 | }
149 | return context;
150 | };
151 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/content-based-on-title.tsx:
--------------------------------------------------------------------------------
1 | import { AccordionContent } from "@/components/ui/accordion";
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardHeader,
7 | CardTitle,
8 | } from "@/components/ui/card";
9 | import { Input } from "@/components/ui/input";
10 | import { onContentChange } from "@/lib/editor-utils";
11 | import { nodeMapper } from "@/lib/types";
12 | import { ConnectionProviderProps } from "@/providers/connections-provider";
13 | import { EditorState } from "@/providers/editor-provider";
14 | import axios from "axios";
15 | import { useEffect } from "react";
16 | import { toast } from "sonner";
17 | import ActionButton from "./action-button";
18 | import GoogleDriveFiles from "./google-drive-files";
19 | import GoogleFileDetails from "./google-file-details";
20 |
21 | export interface Option {
22 | value: string;
23 | label: string;
24 | disable?: boolean;
25 | /** fixed option that can't be removed. */
26 | fixed?: boolean;
27 | /** Group the options by providing key. */
28 | [key: string]: string | boolean | undefined;
29 | }
30 | interface GroupOption {
31 | [key: string]: Option[];
32 | }
33 |
34 | type Props = {
35 | nodeConnection: ConnectionProviderProps;
36 | newState: EditorState;
37 | file: any;
38 | setFile: (file: any) => void;
39 | selectedSlackChannels: Option[];
40 | setSelectedSlackChannels: (value: Option[]) => void;
41 | };
42 |
43 | const ContentBasedOnTitle = ({
44 | nodeConnection,
45 | newState,
46 | file,
47 | setFile,
48 | selectedSlackChannels,
49 | setSelectedSlackChannels,
50 | }: Props) => {
51 | const { selectedNode } = newState.editor;
52 | const title = selectedNode.data.title;
53 |
54 | useEffect(() => {
55 | const reqGoogle = async () => {
56 | const response: { data: { message: { files: any } } } =
57 | await axios.get("/api/drive");
58 | if (response) {
59 | console.log(response.data.message.files[0]);
60 | toast.message("Fetched File");
61 | setFile(response.data.message.files[0]);
62 | } else {
63 | toast.error("Something went wrong");
64 | }
65 | };
66 | reqGoogle();
67 | }, []);
68 |
69 | // @ts-ignore
70 | const nodeConnectionType: any = nodeConnection[nodeMapper[title]];
71 | if (!nodeConnectionType) return Not connected
;
72 |
73 | const isConnected =
74 | title === "Google Drive"
75 | ? !nodeConnection.isLoading
76 | : !!nodeConnectionType[
77 | `${
78 | title === "Slack"
79 | ? "slackAccessToken"
80 | : title === "Discord"
81 | ? "webhookURL"
82 | : title === "Notion"
83 | ? "accessToken"
84 | : ""
85 | }`
86 | ];
87 |
88 | if (!isConnected) return Not connected
;
89 |
90 | return (
91 |
92 |
93 | {title === "Discord" && (
94 |
95 | {nodeConnectionType.webhookName}
96 | {nodeConnectionType.guildName}
97 |
98 | )}
99 |
100 |
{title === "Notion" ? "Values to be stored" : "Message"}
101 |
102 |
onContentChange(nodeConnection, title, event)}
106 | />
107 |
108 | {JSON.stringify(file) !== "{}" && title !== "Google Drive" && (
109 |
110 |
111 |
112 |
Drive File
113 |
114 |
119 |
120 |
121 |
122 |
123 | )}
124 | {title === "Google Drive" &&
}
125 |
131 |
132 |
133 |
134 | );
135 | };
136 |
137 | export default ContentBasedOnTitle;
138 |
--------------------------------------------------------------------------------
/src/app/api/drive-activity/notification/route.ts:
--------------------------------------------------------------------------------
1 | import { postContentToWebHook } from "@/app/(main)/(pages)/connections/_actions/discord-connection";
2 | import { onCreateNewPageInDatabase } from "@/app/(main)/(pages)/connections/_actions/notion-connection";
3 | import { postMessageToSlack } from "@/app/(main)/(pages)/connections/_actions/slack-connection";
4 | import { db } from "@/lib/db";
5 | import axios from "axios";
6 | import { headers } from "next/headers";
7 | import { NextRequest } from "next/server";
8 |
9 | export async function POST(req: NextRequest) {
10 | console.log("🔴 Changed");
11 | const headersList = headers();
12 | let channelResourceId;
13 | headersList.forEach((value, key) => {
14 | if (key == "x-goog-resource-id") {
15 | channelResourceId = value;
16 | }
17 | });
18 |
19 | if (channelResourceId) {
20 | const user = await db.user.findFirst({
21 | where: {
22 | googleResourceId: channelResourceId,
23 | },
24 | select: { clerkId: true, credits: true },
25 | });
26 | if ((user && parseInt(user.credits!) > 0) || user?.credits == "Unlimited") {
27 | const workflow = await db.workflows.findMany({
28 | where: {
29 | userId: user.clerkId,
30 | },
31 | });
32 | if (workflow) {
33 | workflow.map(async (flow) => {
34 | const flowPath = JSON.parse(flow.flowPath!);
35 | let current = 0;
36 | while (current < flowPath.length) {
37 | if (flowPath[current] == "Discord") {
38 | const discordMessage = await db.discordWebhook.findFirst({
39 | where: {
40 | userId: flow.userId,
41 | },
42 | select: {
43 | url: true,
44 | },
45 | });
46 | if (discordMessage) {
47 | await postContentToWebHook(
48 | flow.discordTemplate!,
49 | discordMessage.url,
50 | );
51 | flowPath.splice(flowPath[current], 1);
52 | }
53 | }
54 | if (flowPath[current] == "Slack") {
55 | const channels = flow.slackChannels.map((channel) => {
56 | return {
57 | label: "",
58 | value: channel,
59 | };
60 | });
61 | await postMessageToSlack(
62 | flow.slackAccessToken!,
63 | channels,
64 | flow.slackTemplate!,
65 | );
66 | flowPath.splice(flowPath[current], 1);
67 | }
68 | if (flowPath[current] == "Notion") {
69 | await onCreateNewPageInDatabase(
70 | flow.notionDbId!,
71 | flow.notionAccessToken!,
72 | JSON.parse(flow.notionTemplate!),
73 | );
74 | flowPath.splice(flowPath[current], 1);
75 | }
76 |
77 | if (flowPath[current] == "Wait") {
78 | const res = await axios.put(
79 | "https://api.cron-job.org/jobs",
80 | {
81 | job: {
82 | url: `${process.env.NGROK_URI}?flow_id=${flow.id}`,
83 | enabled: "true",
84 | schedule: {
85 | timezone: "Europe/Istanbul",
86 | expiresAt: 0,
87 | hours: [-1],
88 | mdays: [-1],
89 | minutes: ["*****"],
90 | months: [-1],
91 | wdays: [-1],
92 | },
93 | },
94 | },
95 | {
96 | headers: {
97 | Authorization: `Bearer ${process.env.CRON_JOB_KEY!}`,
98 | "Content-Type": "application/json",
99 | },
100 | },
101 | );
102 | if (res) {
103 | flowPath.splice(flowPath[current], 1);
104 | const cronPath = await db.workflows.update({
105 | where: {
106 | id: flow.id,
107 | },
108 | data: {
109 | cronPath: JSON.stringify(flowPath),
110 | },
111 | });
112 | if (cronPath) break;
113 | }
114 | break;
115 | }
116 | current++;
117 | }
118 |
119 | await db.user.update({
120 | where: {
121 | clerkId: user.clerkId,
122 | },
123 | data: {
124 | credits: `${parseInt(user.credits!) - 1}`,
125 | },
126 | });
127 | });
128 | return Response.json(
129 | {
130 | message: "flow completed",
131 | },
132 | {
133 | status: 200,
134 | },
135 | );
136 | }
137 | }
138 | }
139 | return Response.json(
140 | {
141 | message: "success",
142 | },
143 | {
144 | status: 200,
145 | },
146 | );
147 | }
148 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/_actions/workflow-connections.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 | import type { Option } from "@/components/ui/multiple-selector";
3 | import { db } from "@/lib/db";
4 | import { auth, currentUser } from "@clerk/nextjs/server";
5 |
6 | export const getGoogleListener = async () => {
7 | const { userId } = auth();
8 |
9 | if (userId) {
10 | const listener = await db.user.findUnique({
11 | where: {
12 | clerkId: userId,
13 | },
14 | select: {
15 | googleResourceId: true,
16 | },
17 | });
18 |
19 | if (listener) return listener;
20 | }
21 | };
22 |
23 | export const onFlowPublish = async (workflowId: string, state: boolean) => {
24 | console.log(state);
25 | const published = await db.workflows.update({
26 | where: {
27 | id: workflowId,
28 | },
29 | data: {
30 | publish: state,
31 | },
32 | });
33 |
34 | if (published.publish) return "Workflow published";
35 | return "Workflow unpublished";
36 | };
37 |
38 | export const onCreateNodeTemplate = async (
39 | content: string,
40 | type: string,
41 | workflowId: string,
42 | channels?: Option[],
43 | accessToken?: string,
44 | notionDbId?: string,
45 | ) => {
46 | if (type === "Discord") {
47 | const response = await db.workflows.update({
48 | where: {
49 | id: workflowId,
50 | },
51 | data: {
52 | discordTemplate: content,
53 | },
54 | });
55 |
56 | if (response) {
57 | return "Discord template saved";
58 | }
59 | }
60 | if (type === "Slack") {
61 | const response = await db.workflows.update({
62 | where: {
63 | id: workflowId,
64 | },
65 | data: {
66 | slackTemplate: content,
67 | slackAccessToken: accessToken,
68 | },
69 | });
70 |
71 | if (response) {
72 | const channelList = await db.workflows.findUnique({
73 | where: {
74 | id: workflowId,
75 | },
76 | select: {
77 | slackChannels: true,
78 | },
79 | });
80 |
81 | if (channelList) {
82 | //remove duplicates before insert
83 | const NonDuplicated = channelList.slackChannels.filter(
84 | (channel) => channel !== channels![0].value,
85 | );
86 |
87 | NonDuplicated!
88 | .map((channel) => channel)
89 | .forEach(async (channel) => {
90 | await db.workflows.update({
91 | where: {
92 | id: workflowId,
93 | },
94 | data: {
95 | slackChannels: {
96 | push: channel,
97 | },
98 | },
99 | });
100 | });
101 |
102 | return "Slack template saved";
103 | }
104 | channels!
105 | .map((channel) => channel.value)
106 | .forEach(async (channel) => {
107 | await db.workflows.update({
108 | where: {
109 | id: workflowId,
110 | },
111 | data: {
112 | slackChannels: {
113 | push: channel,
114 | },
115 | },
116 | });
117 | });
118 | return "Slack template saved";
119 | }
120 | }
121 |
122 | if (type === "Notion") {
123 | const response = await db.workflows.update({
124 | where: {
125 | id: workflowId,
126 | },
127 | data: {
128 | notionTemplate: content,
129 | notionAccessToken: accessToken,
130 | notionDbId: notionDbId,
131 | },
132 | });
133 |
134 | if (response) return "Notion template saved";
135 | }
136 | };
137 |
138 | export const onGetWorkflows = async () => {
139 | const user = await currentUser();
140 | if (user) {
141 | const workflow = await db.workflows.findMany({
142 | where: {
143 | userId: user.id,
144 | },
145 | });
146 |
147 | if (workflow) return workflow;
148 | }
149 | };
150 |
151 | export const onCreateWorkflow = async (name: string, description: string) => {
152 | const user = await currentUser();
153 |
154 | if (user) {
155 | //create new workflow
156 | const workflow = await db.workflows.create({
157 | data: {
158 | userId: user.id,
159 | name,
160 | description,
161 | },
162 | });
163 |
164 | if (workflow) return { message: "workflow created" };
165 | return { message: "Oops! try again" };
166 | }
167 | };
168 |
169 | export const onGetNodesEdges = async (flowId: string) => {
170 | const nodesEdges = await db.workflows.findUnique({
171 | where: {
172 | id: flowId,
173 | },
174 | select: {
175 | nodes: true,
176 | edges: true,
177 | },
178 | });
179 | if (nodesEdges?.nodes && nodesEdges?.edges) return nodesEdges;
180 | };
181 |
--------------------------------------------------------------------------------
/src/app/(main)/(pages)/workflows/editor/[editorId]/_components/editor-canvas-sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
3 | import { EditorCanvasTypes, EditorNodeType } from "@/lib/types";
4 | import { useNodeConnections } from "@/providers/connections-provider";
5 | import { useEditor } from "@/providers/editor-provider";
6 |
7 | import {
8 | Accordion,
9 | AccordionContent,
10 | AccordionItem,
11 | AccordionTrigger,
12 | } from "@/components/ui/accordion";
13 | import {
14 | Card,
15 | CardDescription,
16 | CardHeader,
17 | CardTitle,
18 | } from "@/components/ui/card";
19 | import { Separator } from "@/components/ui/separator";
20 | import { CONNECTIONS, EditorCanvasDefaultCardTypes } from "@/lib/constant";
21 | import {
22 | fetchBotSlackChannels,
23 | onConnections,
24 | onDragStart,
25 | } from "@/lib/editor-utils";
26 | import { useFuzzieStore } from "@/store";
27 | import { useEffect } from "react";
28 | import EditorCanvasIconHelper from "./editor-canvas-card-icon-hepler";
29 | import RenderConnectionAccordion from "./render-connection-accordion";
30 | import RenderOutputAccordion from "./render-output-accordian";
31 |
32 | type Props = {
33 | nodes: EditorNodeType[];
34 | };
35 |
36 | const EditorCanvasSidebar = ({ nodes }: Props) => {
37 | const { state } = useEditor();
38 | const { nodeConnection } = useNodeConnections();
39 | const { googleFile, setSlackChannels } = useFuzzieStore();
40 | useEffect(() => {
41 | if (state) {
42 | onConnections(nodeConnection, state, googleFile);
43 | }
44 | }, [state]);
45 |
46 | useEffect(() => {
47 | if (nodeConnection.slackNode.slackAccessToken) {
48 | fetchBotSlackChannels(
49 | nodeConnection.slackNode.slackAccessToken,
50 | setSlackChannels,
51 | );
52 | }
53 | }, [nodeConnection]);
54 |
55 | return (
56 |
57 |
58 |
59 | Actions
60 | Settings
61 |
62 |
63 |
64 | {Object.entries(EditorCanvasDefaultCardTypes)
65 | .filter(
66 | ([_, cardType]) =>
67 | (!nodes.length && cardType.type === "Trigger") ||
68 | (nodes.length && cardType.type === "Action"),
69 | )
70 | .map(([cardKey, cardValue]) => (
71 |
76 | onDragStart(event, cardKey as EditorCanvasTypes)
77 | }
78 | >
79 |
80 |
81 |
82 | {cardKey}
83 | {cardValue.description}
84 |
85 |
86 |
87 | ))}
88 |
89 |
90 |
91 | {state.editor.selectedNode.data.title}
92 |
93 |
94 |
95 |
96 |
97 | Account
98 |
99 |
100 | {CONNECTIONS.map((connection) => (
101 |
106 | ))}
107 |
108 |
109 |
110 |
111 | Action
112 |
113 |
117 |
118 |
119 |
120 |
121 |
122 | );
123 | };
124 |
125 | export default EditorCanvasSidebar;
126 |
--------------------------------------------------------------------------------
/src/components/global/connect-parallax.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import {
3 | MotionValue,
4 | motion,
5 | useScroll,
6 | useSpring,
7 | useTransform,
8 | } from "framer-motion";
9 | import Image from "next/image";
10 | import Link from "next/link";
11 | import React from "react";
12 |
13 | export const HeroParallax = ({
14 | products,
15 | }: {
16 | products: {
17 | title: string;
18 | link: string;
19 | thumbnail: string;
20 | }[];
21 | }) => {
22 | const firstRow = products.slice(0, 5);
23 | const secondRow = products.slice(5, 10);
24 | const thirdRow = products.slice(10, 15);
25 | const ref = React.useRef(null);
26 | const { scrollYProgress } = useScroll({
27 | target: ref,
28 | offset: ["start start", "end start"],
29 | });
30 |
31 | const springConfig = { stiffness: 300, damping: 30, bounce: 100 };
32 |
33 | const translateX = useSpring(
34 | useTransform(scrollYProgress, [0, 1], [0, 1000]),
35 | springConfig,
36 | );
37 | const translateXReverse = useSpring(
38 | useTransform(scrollYProgress, [0, 1], [0, -1000]),
39 | springConfig,
40 | );
41 | const rotateX = useSpring(
42 | useTransform(scrollYProgress, [0, 0.2], [15, 0]),
43 | springConfig,
44 | );
45 | const opacity = useSpring(
46 | useTransform(scrollYProgress, [0, 0.2], [0.2, 1]),
47 | springConfig,
48 | );
49 | const rotateZ = useSpring(
50 | useTransform(scrollYProgress, [0, 0.2], [20, 0]),
51 | springConfig,
52 | );
53 | const translateY = useSpring(
54 | useTransform(scrollYProgress, [0, 0.2], [-700, 500]),
55 | springConfig,
56 | );
57 | return (
58 |
62 |
63 |
72 |
73 | {firstRow.map((product) => (
74 |
79 | ))}
80 |
81 |
82 | {secondRow.map((product) => (
83 |
88 | ))}
89 |
90 |
91 | {thirdRow.map((product) => (
92 |
97 | ))}
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | export const Header = () => {
105 | return (
106 |
107 |
108 | The Ultimate development studio
109 |
110 |
111 | We build beautiful products with the latest technologies and frameworks.
112 | We are a team of passionate developers and designers that love to build
113 | amazing products.
114 |
115 |
116 | );
117 | };
118 |
119 | export const ProductCard = ({
120 | product,
121 | translate,
122 | }: {
123 | product: {
124 | title: string;
125 | link: string;
126 | thumbnail: string;
127 | };
128 | translate: MotionValue;
129 | }) => {
130 | return (
131 |
141 |
145 |
152 |
153 |
154 |
155 | {product.title}
156 |
157 |
158 | );
159 | };
160 |
--------------------------------------------------------------------------------
/src/components/global/lamp.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { cn } from "@/lib/utils";
3 | import { motion } from "framer-motion";
4 | import React from "react";
5 | import { SparklesCore } from "./sparkles";
6 |
7 | export function LampComponent() {
8 | return (
9 |
10 |
20 | Plans That
21 | Fit You Best
22 |
23 |
24 | );
25 | }
26 |
27 | export const LampContainer = ({
28 | children,
29 | className,
30 | }: {
31 | children: React.ReactNode;
32 | className?: string;
33 | }) => {
34 | return (
35 |
41 |
42 |
55 |
56 |
57 |
58 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
87 |
97 |
98 |
99 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 |
116 | );
117 | };
118 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss'
2 |
3 | const config = {
4 | darkMode: ['class'],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: '',
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: '2rem',
16 | screens: {
17 | '2xl': '1400px',
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: 'hsl(var(--border))',
23 | input: 'hsl(var(--input))',
24 | ring: 'hsl(var(--ring))',
25 | background: 'hsl(var(--background))',
26 | foreground: 'hsl(var(--foreground))',
27 | primary: {
28 | DEFAULT: 'hsl(var(--primary))',
29 | foreground: 'hsl(var(--primary-foreground))',
30 | },
31 | secondary: {
32 | DEFAULT: 'hsl(var(--secondary))',
33 | foreground: 'hsl(var(--secondary-foreground))',
34 | },
35 | destructive: {
36 | DEFAULT: 'hsl(var(--destructive))',
37 | foreground: 'hsl(var(--destructive-foreground))',
38 | },
39 | muted: {
40 | DEFAULT: 'hsl(var(--muted))',
41 | foreground: 'hsl(var(--muted-foreground))',
42 | },
43 | accent: {
44 | DEFAULT: 'hsl(var(--accent))',
45 | foreground: 'hsl(var(--accent-foreground))',
46 | },
47 | popover: {
48 | DEFAULT: 'hsl(var(--popover))',
49 | foreground: 'hsl(var(--popover-foreground))',
50 | },
51 | card: {
52 | DEFAULT: 'hsl(var(--card))',
53 | foreground: 'hsl(var(--card-foreground))',
54 | },
55 | },
56 | borderRadius: {
57 | lg: 'var(--radius)',
58 | md: 'calc(var(--radius) - 2px)',
59 | sm: 'calc(var(--radius) - 4px)',
60 | },
61 | keyframes: {
62 | scroll: {
63 | to: {
64 | transform: 'translate(calc(-50% - 0.5rem))',
65 | },
66 | },
67 | spotlight: {
68 | '0%': {
69 | opacity: '0',
70 | transform: 'translate(-72%, -62%) scale(0.5)',
71 | },
72 | '100%': {
73 | opacity: '1',
74 | transform: 'translate(-50%,-40%) scale(1)',
75 | },
76 | },
77 | moveHorizontal: {
78 | '0%': {
79 | transform: 'translateX(-50%) translateY(-10%)',
80 | },
81 | '50%': {
82 | transform: 'translateX(50%) translateY(10%)',
83 | },
84 | '100%': {
85 | transform: 'translateX(-50%) translateY(-10%)',
86 | },
87 | },
88 | moveInCircle: {
89 | '0%': {
90 | transform: 'rotate(0deg)',
91 | },
92 | '50%': {
93 | transform: 'rotate(180deg)',
94 | },
95 | '100%': {
96 | transform: 'rotate(360deg)',
97 | },
98 | },
99 | moveVertical: {
100 | '0%': {
101 | transform: 'translateY(-50%)',
102 | },
103 | '50%': {
104 | transform: 'translateY(50%)',
105 | },
106 | '100%': {
107 | transform: 'translateY(-50%)',
108 | },
109 | },
110 | 'accordion-down': {
111 | from: { height: '0' },
112 | to: { height: 'var(--radix-accordion-content-height)' },
113 | },
114 | 'accordion-up': {
115 | from: { height: 'var(--radix-accordion-content-height)' },
116 | to: { height: '0' },
117 | },
118 | },
119 | animation: {
120 | scroll:
121 | 'scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite',
122 | spotlight: 'spotlight 2s ease .75s 1 forwards',
123 | 'accordion-down': 'accordion-down 0.2s ease-out',
124 | 'accordion-up': 'accordion-up 0.2s ease-out',
125 | first: 'moveVertical 30s ease infinite',
126 | second: 'moveInCircle 20s reverse infinite',
127 | third: 'moveInCircle 40s linear infinite',
128 | fourth: 'moveHorizontal 40s ease infinite',
129 | fifth: 'moveInCircle 20s ease infinite',
130 | },
131 | },
132 | },
133 | plugins: [require('tailwindcss-animate')],
134 | } satisfies Config
135 |
136 | // function addVariablesForColors({ addBase, theme }: any) {
137 | // let allColors = flattenColorPalette(theme('colors'))
138 | // let newVars = Object.fromEntries(
139 | // Object.entries(allColors).map(([key, val]) => [`--${key}`, val])
140 | // )
141 | // addBase({
142 | // ':root': newVars,
143 | // })
144 | // }
145 |
146 | export default config
147 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as LabelPrimitive from "@radix-ui/react-label";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import * as React from "react";
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form";
12 |
13 | import { Label } from "@/components/ui/label";
14 | import { cn } from "@/lib/utils";
15 |
16 | const Form = FormProvider;
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath,
21 | > = {
22 | name: TName;
23 | };
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue,
27 | );
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath,
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext);
44 | const itemContext = React.useContext(FormItemContext);
45 | const { getFieldState, formState } = useFormContext();
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState);
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ");
51 | }
52 |
53 | const { id } = itemContext;
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | };
63 | };
64 |
65 | type FormItemContextValue = {
66 | id: string;
67 | };
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue,
71 | );
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId();
78 |
79 | return (
80 |
81 |
82 |
83 | );
84 | });
85 | FormItem.displayName = "FormItem";
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField();
92 |
93 | return (
94 |
100 | );
101 | });
102 | FormLabel.displayName = "FormLabel";
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } =
109 | useFormField();
110 |
111 | return (
112 |
123 | );
124 | });
125 | FormControl.displayName = "FormControl";
126 |
127 | const FormDescription = React.forwardRef<
128 | HTMLParagraphElement,
129 | React.HTMLAttributes
130 | >(({ className, ...props }, ref) => {
131 | const { formDescriptionId } = useFormField();
132 |
133 | return (
134 |
140 | );
141 | });
142 | FormDescription.displayName = "FormDescription";
143 |
144 | const FormMessage = React.forwardRef<
145 | HTMLParagraphElement,
146 | React.HTMLAttributes
147 | >(({ className, children, ...props }, ref) => {
148 | const { error, formMessageId } = useFormField();
149 | const body = error ? String(error?.message) : children;
150 |
151 | if (!body) {
152 | return null;
153 | }
154 |
155 | return (
156 |
162 | {body}
163 |
164 | );
165 | });
166 | FormMessage.displayName = "FormMessage";
167 |
168 | export {
169 | useFormField,
170 | Form,
171 | FormItem,
172 | FormLabel,
173 | FormControl,
174 | FormDescription,
175 | FormMessage,
176 | FormField,
177 | };
178 |
--------------------------------------------------------------------------------
/src/lib/editor-utils.ts:
--------------------------------------------------------------------------------
1 | import { getDiscordConnectionUrl } from "@/app/(main)/(pages)/connections/_actions/discord-connection";
2 | import {
3 | getNotionConnection,
4 | getNotionDatabase,
5 | } from "@/app/(main)/(pages)/connections/_actions/notion-connection";
6 | import {
7 | getSlackConnection,
8 | listBotChannels,
9 | } from "@/app/(main)/(pages)/connections/_actions/slack-connection";
10 | import { Option } from "@/components/ui/multiple-selector";
11 | import { ConnectionProviderProps } from "@/providers/connections-provider";
12 | import { EditorState } from "@/providers/editor-provider";
13 | import { EditorCanvasCardType } from "./types";
14 |
15 | export const onDragStart = (
16 | event: any,
17 | nodeType: EditorCanvasCardType["type"],
18 | ) => {
19 | event.dataTransfer.setData("application/reactflow", nodeType);
20 | event.dataTransfer.effectAllowed = "move";
21 | };
22 |
23 | export const onSlackContent = (
24 | nodeConnection: ConnectionProviderProps,
25 | event: React.ChangeEvent,
26 | ) => {
27 | nodeConnection.setSlackNode((prev: any) => ({
28 | ...prev,
29 | content: event.target.value,
30 | }));
31 | };
32 |
33 | export const onDiscordContent = (
34 | nodeConnection: ConnectionProviderProps,
35 | event: React.ChangeEvent,
36 | ) => {
37 | nodeConnection.setDiscordNode((prev: any) => ({
38 | ...prev,
39 | content: event.target.value,
40 | }));
41 | };
42 |
43 | export const onContentChange = (
44 | nodeConnection: ConnectionProviderProps,
45 | nodeType: string,
46 | event: React.ChangeEvent,
47 | ) => {
48 | if (nodeType === "Slack") {
49 | onSlackContent(nodeConnection, event);
50 | } else if (nodeType === "Discord") {
51 | onDiscordContent(nodeConnection, event);
52 | } else if (nodeType === "Notion") {
53 | onNotionContent(nodeConnection, event);
54 | }
55 | };
56 |
57 | export const onAddTemplateSlack = (
58 | nodeConnection: ConnectionProviderProps,
59 | template: string,
60 | ) => {
61 | nodeConnection.setSlackNode((prev: any) => ({
62 | ...prev,
63 | content: `${prev.content} ${template}`,
64 | }));
65 | };
66 |
67 | export const onAddTemplateDiscord = (
68 | nodeConnection: ConnectionProviderProps,
69 | template: string,
70 | ) => {
71 | nodeConnection.setDiscordNode((prev: any) => ({
72 | ...prev,
73 | content: `${prev.content} ${template}`,
74 | }));
75 | };
76 |
77 | export const onAddTemplate = (
78 | nodeConnection: ConnectionProviderProps,
79 | title: string,
80 | template: string,
81 | ) => {
82 | if (title === "Slack") {
83 | onAddTemplateSlack(nodeConnection, template);
84 | } else if (title === "Discord") {
85 | onAddTemplateDiscord(nodeConnection, template);
86 | }
87 | };
88 |
89 | export const onConnections = async (
90 | nodeConnection: ConnectionProviderProps,
91 | editorState: EditorState,
92 | googleFile: any,
93 | ) => {
94 | if (editorState.editor.selectedNode.data.title == "Discord") {
95 | const connection = await getDiscordConnectionUrl();
96 | if (connection) {
97 | nodeConnection.setDiscordNode({
98 | webhookURL: connection.url,
99 | content: "",
100 | webhookName: connection.name,
101 | guildName: connection.guildName,
102 | });
103 | }
104 | }
105 | if (editorState.editor.selectedNode.data.title == "Notion") {
106 | const connection = await getNotionConnection();
107 | if (connection) {
108 | nodeConnection.setNotionNode({
109 | accessToken: connection.accessToken,
110 | databaseId: connection.databaseId,
111 | workspaceName: connection.workspaceName,
112 | content: {
113 | name: googleFile.name,
114 | kind: googleFile.kind,
115 | type: googleFile.mimeType,
116 | },
117 | });
118 |
119 | if (nodeConnection.notionNode.databaseId !== "") {
120 | const response = await getNotionDatabase(
121 | nodeConnection.notionNode.databaseId,
122 | nodeConnection.notionNode.accessToken,
123 | );
124 | }
125 | }
126 | }
127 | if (editorState.editor.selectedNode.data.title == "Slack") {
128 | const connection = await getSlackConnection();
129 | if (connection) {
130 | nodeConnection.setSlackNode({
131 | appId: connection.appId,
132 | authedUserId: connection.authedUserId,
133 | authedUserToken: connection.authedUserToken,
134 | slackAccessToken: connection.slackAccessToken,
135 | botUserId: connection.botUserId,
136 | teamId: connection.teamId,
137 | teamName: connection.teamName,
138 | userId: connection.userId,
139 | content: "",
140 | });
141 | }
142 | }
143 | };
144 |
145 | export const fetchBotSlackChannels = async (
146 | token: string,
147 | setSlackChannels: (slackChannels: Option[]) => void,
148 | ) => {
149 | await listBotChannels(token)?.then((channels) => setSlackChannels(channels));
150 | };
151 |
152 | export const onNotionContent = (
153 | nodeConnection: ConnectionProviderProps,
154 | event: React.ChangeEvent,
155 | ) => {
156 | nodeConnection.setNotionNode((prev: any) => ({
157 | ...prev,
158 | content: event.target.value,
159 | }));
160 | };
161 |
--------------------------------------------------------------------------------