├── .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 | 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 | 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 | User_Image 32 |
33 | 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 | 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 | {title} 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 | 63 | 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 | 37 | 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 | Google Drive 46 | Google Drive 53 | Google Drive 60 |
61 |
62 | {name} 63 | {description} 64 |
65 | 66 |
67 |
68 | 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 | 59 | ) : ( 60 | 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 |
41 |
47 |
48 | 49 |
50 |
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 | bannerImage 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 |
50 | 54 | ( 59 | 60 | User full name 61 | 62 | 63 | 64 | 65 | 66 | )} 67 | /> 68 | ( 72 | 73 | Email 74 | 75 | 81 | 82 | 83 | 84 | )} 85 | /> 86 | 99 | 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 | {item.href} 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 |
48 | 64 |
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 | 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 |
66 | 70 | ( 75 | 76 | Name 77 | 78 | 79 | 80 | 81 | 82 | )} 83 | /> 84 | ( 89 | 90 | Description 91 | 92 | 93 | 94 | 95 | 96 | )} 97 | /> 98 | 107 | 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 | 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 | 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 | {product.title} 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 |