├── .eslintrc.json ├── app ├── default.tsx ├── @modal │ ├── default.tsx │ └── (..)plugin │ │ └── [name] │ │ └── page.tsx ├── globals.css ├── favicon.ico ├── api │ └── page.tsx ├── chat │ └── page.tsx ├── layout.tsx ├── plugin │ └── [name] │ │ └── page.tsx ├── about │ └── page.tsx ├── page.tsx └── submit │ └── page.tsx ├── public ├── og.png ├── img.jpeg ├── logo.png ├── fallback.svg ├── vercel.svg ├── logo.svg ├── thirteen.svg └── next.svg ├── postcss.config.js ├── prisma ├── migrations │ ├── 20230406144138_no_unique_submissions │ │ └── migration.sql │ ├── migration_lock.toml │ ├── 20230406090141_make_url_unique │ │ └── migration.sql │ ├── 20230406145534_make_name_unique │ │ └── migration.sql │ ├── 20230406050400_intialize │ │ └── migration.sql │ ├── 20230406144014_add_submissions_table │ │ └── migration.sql │ └── 20230411144538_add_os_plugins │ │ └── migration.sql └── schema.prisma ├── .vscode └── settings.json ├── next.config.js ├── lib ├── utils.ts ├── prisma.ts └── debounce.ts ├── tailwind.config.js ├── components ├── ui │ ├── PluginCardSkeleton.tsx │ ├── Separator.tsx │ ├── PluginCard.tsx │ ├── DeveloperPluginCard.tsx │ ├── ScrollArea.tsx │ └── PluginDialog.tsx ├── Footer.tsx ├── Navbar.tsx └── PluginContent.tsx ├── pages └── api │ ├── getOSPlugins.ts │ ├── getPlugins.ts │ ├── getByName.ts │ ├── search.ts │ ├── fetch.ts │ ├── og.tsx │ └── submit.ts ├── .gitignore ├── tsconfig.json ├── README.md └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /app/@modal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnerkar/gpt-plugins/HEAD/public/og.png -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnerkar/gpt-plugins/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /public/img.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnerkar/gpt-plugins/HEAD/public/img.jpeg -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krishnerkar/gpt-plugins/HEAD/public/logo.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/20230406144138_no_unique_submissions/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "PluginSubmissions_url_key"; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /public/fallback.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined; 5 | } 6 | 7 | const prisma = global.prisma || new PrismaClient(); 8 | 9 | if (process.env.NODE_ENV === "development") global.prisma = prisma; 10 | 11 | export default prisma; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230406090141_make_url_unique/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[url]` on the table `Plugin` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Plugin_url_key" ON "Plugin"("url"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20230406145534_make_name_unique/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[name]` on the table `Plugin` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "Plugin_name_key" ON "Plugin"("name"); 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: "class", 4 | content: [ 5 | "./app/**/*.{js,ts,jsx,tsx}", 6 | "./pages/**/*.{js,ts,jsx,tsx}", 7 | "./components/**/*.{js,ts,jsx,tsx}", 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [require("@tailwindcss/forms")], 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /lib/debounce.ts: -------------------------------------------------------------------------------- 1 | export default function debounce( 2 | fn: (...args: any[]) => void, 3 | delay: number 4 | ): (...args: any[]) => void { 5 | let timeoutId: NodeJS.Timeout | null; 6 | return function (...args: any[]): void { 7 | if (timeoutId) { 8 | clearTimeout(timeoutId); 9 | } 10 | timeoutId = setTimeout(() => { 11 | fn(...args); 12 | }, delay); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /prisma/migrations/20230406050400_intialize/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Plugin" ( 3 | "id" SERIAL NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "description" TEXT NOT NULL, 6 | "logo" TEXT NOT NULL, 7 | "url" TEXT NOT NULL, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" TIMESTAMP(3) NOT NULL, 10 | 11 | CONSTRAINT "Plugin_pkey" PRIMARY KEY ("id") 12 | ); 13 | -------------------------------------------------------------------------------- /components/ui/PluginCardSkeleton.tsx: -------------------------------------------------------------------------------- 1 | export default function PluginCardSkeleton() { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 | ); 10 | } -------------------------------------------------------------------------------- /pages/api/getOSPlugins.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "@/lib/prisma"; 3 | 4 | export type SimpleOSPlugin = { 5 | id: number; 6 | name: string; 7 | description: string; 8 | githubURL: string; 9 | }; 10 | 11 | export default async function handler( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ) { 15 | const oSplugins = await prisma.oSPlugin.findMany({ 16 | orderBy: { id: "desc" }, 17 | }); 18 | res.status(200).json(oSplugins); 19 | } 20 | -------------------------------------------------------------------------------- /pages/api/getPlugins.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "@/lib/prisma"; 3 | 4 | export type SimplePlugin = { 5 | id: number; 6 | name: string; 7 | description: string; 8 | logo: string; 9 | url: string; 10 | }; 11 | 12 | export default async function handler( 13 | req: NextApiRequest, 14 | res: NextApiResponse 15 | ) { 16 | const plugins = await prisma.plugin.findMany({ 17 | orderBy: { id: "desc" }, 18 | }); 19 | res.status(200).json(plugins); 20 | } 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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /prisma/migrations/20230406144014_add_submissions_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "PluginSubmissions" ( 3 | "id" SERIAL NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "description" TEXT NOT NULL, 6 | "logo" TEXT NOT NULL, 7 | "url" TEXT NOT NULL, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" TIMESTAMP(3) NOT NULL, 10 | 11 | CONSTRAINT "PluginSubmissions_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE UNIQUE INDEX "PluginSubmissions_url_key" ON "PluginSubmissions"("url"); 16 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/api/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Footer from "@/components/Footer"; 4 | import Navbar from "@/components/Navbar"; 5 | 6 | export default function API() { 7 | return ( 8 |
9 | 10 |
11 |
12 |

13 | API is coming soon! 14 |

15 |
16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Footer from "@/components/Footer"; 4 | import Navbar from "@/components/Navbar"; 5 | 6 | export default function Chat() { 7 | return ( 8 |
9 | 10 |
11 | 12 |
13 |

14 | Chat is coming soon! 15 |

16 |
17 |
18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /pages/api/getByName.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "@/lib/prisma"; 3 | import { SimplePlugin } from "./getPlugins"; 4 | 5 | export type Result = { 6 | data?: SimplePlugin | null; 7 | error?: string; 8 | }; 9 | 10 | export default async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse 13 | ) { 14 | if (!req.query.name) { 15 | return res.status(400).json({ error: "name parameter is required" }); 16 | } 17 | 18 | const plugin = await prisma.plugin.findUnique({ 19 | where: { name: req.query.name as string }, 20 | }); 21 | 22 | if (!plugin) return res.status(404).json({ error: "Plugin not found" }); 23 | 24 | res.status(200).json({ data: plugin }); 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts", 31 | "app/home/[pasge].tsd" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to GPT Plugins 👋

2 |

3 | Version 4 | 5 | Twitter: https://twitter.com/krishnerkar 6 | 7 |

8 | 9 | 10 | > A directory of all chatgpt plugins 11 | 12 | ### 🏠 [Homepage](https://gptplugins.app) 13 | 14 | ## Install 15 | 16 | ```sh 17 | npm install 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```sh 23 | npm run dev 24 | ``` 25 | 26 | ## Author 27 | 28 | 👤 **Krish Nerkar** 29 | 30 | * Twitter: [@krishnerkar](https://twitter.com/krishnerkar) 31 | * Github: [@krishnerkar](https://github.com/krishnerkar) 32 | 33 | ## Show your support 34 | 35 | Give a ⭐️ if this project helped you! 36 | 37 | -------------------------------------------------------------------------------- /components/ui/Separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Plugin { 14 | id Int @default(autoincrement()) @id 15 | name String @unique 16 | description String 17 | logo String 18 | url String @unique 19 | approved Boolean @default(false) 20 | createdAt DateTime @default(now()) 21 | updatedAt DateTime @updatedAt 22 | } 23 | 24 | model OSPlugin{ 25 | id Int @default(autoincrement()) @id 26 | name String @unique 27 | description String 28 | githubURL String @unique 29 | approved Boolean @default(false) 30 | createdAt DateTime @default(now()) 31 | updatedAt DateTime @updatedAt 32 | } 33 | -------------------------------------------------------------------------------- /prisma/migrations/20230411144538_add_os_plugins/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `PluginSubmissions` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Plugin" ADD COLUMN "approved" BOOLEAN NOT NULL DEFAULT false; 9 | 10 | -- DropTable 11 | DROP TABLE "PluginSubmissions"; 12 | 13 | -- CreateTable 14 | CREATE TABLE "OSPlugin" ( 15 | "id" SERIAL NOT NULL, 16 | "name" TEXT NOT NULL, 17 | "description" TEXT NOT NULL, 18 | "githubURL" TEXT NOT NULL, 19 | "approved" BOOLEAN NOT NULL DEFAULT false, 20 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | "updatedAt" TIMESTAMP(3) NOT NULL, 22 | 23 | CONSTRAINT "OSPlugin_pkey" PRIMARY KEY ("id") 24 | ); 25 | 26 | -- CreateIndex 27 | CREATE UNIQUE INDEX "OSPlugin_name_key" ON "OSPlugin"("name"); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "OSPlugin_githubURL_key" ON "OSPlugin"("githubURL"); 31 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/PluginCard.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | /* eslint-disable @next/next/no-img-element */ 4 | export default function PluginCard({ 5 | name, 6 | description, 7 | logo, 8 | }: { 9 | name: string; 10 | description: string; 11 | logo: string; 12 | }) { 13 | const handleImageError = (e: React.SyntheticEvent) => { 14 | const fallbackImageUrl = "/fallback.svg"; // Replace with your fallback image URL 15 | e.currentTarget.src = fallbackImageUrl; 16 | }; 17 | 18 | return ( 19 |
20 | 24 | {`${name}-logo`} 31 |
32 |

{name}

33 |

{description}

34 |
35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { Inter } from "next/font/google"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | 5 | export const metadata = { 6 | title: "GPT Plugins", 7 | description: "A collection of all chatgpt plugins", 8 | twitter: { 9 | card: "summary_large_image", 10 | title: "GPT Plugins", 11 | description: "A collection of all chatgpt plugins", 12 | creator: "@krishnerkar", 13 | images: ["https://www.gptplugins.app/og.png"], 14 | }, 15 | openGraph: { 16 | title: "GPT Plugins", 17 | description: "A collection of all chatgpt plugins", 18 | type: "website", 19 | url: "https://www.gptplugins.app/", 20 | images: [ 21 | { 22 | url: "https://www.gptplugins.app/og.png", 23 | width: 1200, 24 | height: 630, 25 | alt: "GPT Plugins", 26 | }, 27 | ], 28 | }, 29 | }; 30 | 31 | const inter = Inter({ subsets: ["latin"] }); 32 | 33 | export default function RootLayout({ 34 | children, 35 | modal, 36 | }: { 37 | children: React.ReactNode; 38 | modal: React.ReactNode; 39 | }) { 40 | return ( 41 | 42 | 43 | {children} 44 | {modal} 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/api/search.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "@/lib/prisma"; 3 | import { SimplePlugin } from "./getPlugins"; 4 | import { SimpleOSPlugin } from "./getOSPlugins"; 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse 9 | ) { 10 | if (req.query.type == "open") { 11 | const oSplugins = await prisma.oSPlugin.findMany({ 12 | where: { 13 | OR: [ 14 | { 15 | name: { 16 | contains: req.query.q as string, 17 | mode: "insensitive", 18 | }, 19 | }, 20 | { 21 | description: { 22 | contains: req.query.q as string, 23 | mode: "insensitive", 24 | }, 25 | }, 26 | ], 27 | }, 28 | }); 29 | 30 | res.status(200).json(oSplugins); 31 | } else { 32 | const plugins = await prisma.plugin.findMany({ 33 | where: { 34 | OR: [ 35 | { 36 | name: { 37 | contains: req.query.q as string, 38 | mode: "insensitive", 39 | }, 40 | }, 41 | { 42 | description: { 43 | contains: req.query.q as string, 44 | mode: "insensitive", 45 | }, 46 | }, 47 | ], 48 | }, 49 | }); 50 | res.status(200).json(plugins); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gpt-plugins", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.7.13", 13 | "@heroicons/react": "^2.0.17", 14 | "@prisma/client": "^4.8.1", 15 | "@radix-ui/react-alert-dialog": "^1.0.3", 16 | "@radix-ui/react-scroll-area": "^1.0.3", 17 | "@radix-ui/react-separator": "^1.0.2", 18 | "@tailwindcss/forms": "^0.5.3", 19 | "@types/node": "18.15.11", 20 | "@types/react": "18.0.33", 21 | "@types/react-dom": "18.0.11", 22 | "@vercel/analytics": "^0.1.11", 23 | "@vercel/og": "^0.5.1", 24 | "axios": "^1.3.5", 25 | "class-variance-authority": "^0.5.1", 26 | "clipboard-copy": "^4.0.1", 27 | "clsx": "^1.2.1", 28 | "eslint": "8.37.0", 29 | "eslint-config-next": "13.3", 30 | "js-yaml": "^4.1.0", 31 | "lucide-react": "^0.130.1", 32 | "next": "^13.3.1-canary.4", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "readme-md-generator": "^1.0.0", 36 | "sonner": "^0.3.0", 37 | "tailwind-merge": "^1.12.0", 38 | "tailwindcss-animate": "^1.0.5", 39 | "typescript": "5.0.3" 40 | }, 41 | "devDependencies": { 42 | "@types/js-yaml": "^4.0.5", 43 | "autoprefixer": "^10.4.14", 44 | "postcss": "^8.4.21", 45 | "prisma": "^4.12.0", 46 | "tailwindcss": "^3.3.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/plugin/[name]/page.tsx: -------------------------------------------------------------------------------- 1 | import PluginContent from "@/components/PluginContent"; 2 | import { Metadata } from "next"; 3 | 4 | type Props = { 5 | params: { name: string }; 6 | }; 7 | 8 | export async function generateMetadata( 9 | { params }: Props, 10 | ): Promise { 11 | const name = params.name; 12 | 13 | const plugin = await fetch(`https://www.gptplugins.app/api/getByName?name=${name}`).then((res) => 14 | res.json() 15 | ); 16 | return { 17 | title: plugin.data.name, 18 | description: plugin.data.description, 19 | twitter: { 20 | card: "summary_large_image", 21 | title: plugin.data.name, 22 | description: plugin.data.description, 23 | creator: "@krishnerkar", 24 | images: [ 25 | `https://www.gptplugins.app/api/og?name=${plugin.data.name}&description=${plugin.data.description}&logo=${plugin.data.logo}`, 26 | ], 27 | }, 28 | openGraph: { 29 | title: plugin.data.name, 30 | description: plugin.data.description, 31 | type: "website", 32 | url: "https://www.gptplugins.app/", 33 | images: [ 34 | { 35 | url: `https://www.gptplugins.app/api/og?name=${plugin.data.name}&description=${plugin.data.description}&logo=${plugin.data.logo}`, 36 | width: 1200, 37 | height: 630, 38 | alt: "GPT Plugins", 39 | }, 40 | ], 41 | }, 42 | }; 43 | } 44 | 45 | export default function Plugin({ params }: { params: { name: string } }) { 46 | return ; 47 | } 48 | -------------------------------------------------------------------------------- /pages/api/fetch.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "@/lib/prisma"; 3 | import { AiPlugin } from "./submit"; 4 | import yaml from "js-yaml"; 5 | import axios from "axios"; 6 | 7 | type Data = { 8 | status: string; 9 | plugin: string; 10 | openapi: string; 11 | }; 12 | 13 | type Body = { 14 | url: string; 15 | }; 16 | 17 | function YAML2JSON(yamlString: string) { 18 | try { 19 | const jsonObj = yaml.load(yamlString); 20 | return jsonObj; 21 | } catch (error) { 22 | console.error("Something went wrong converting YAML to JSON:", error); 23 | return null; 24 | } 25 | } 26 | 27 | function isYAML(inputString: string) { 28 | const yamlPattern = /:\s|\n:/; 29 | return yamlPattern.test(inputString); 30 | } 31 | 32 | export default async function handler( 33 | req: NextApiRequest, 34 | res: NextApiResponse 35 | ) { 36 | const body = req.body as Body; 37 | const url = body.url; 38 | 39 | const manifestRaw = await fetch(url); 40 | const manifest: AiPlugin = await manifestRaw.json(); 41 | 42 | const openAPIUrl = manifest.api.url; 43 | let openAPI; 44 | 45 | try { 46 | const openAPIRaw = await axios.get(openAPIUrl); 47 | openAPI = isYAML(openAPIRaw.data) 48 | ? YAML2JSON(openAPIRaw.data) 49 | : openAPIRaw.data; 50 | } catch (error) { 51 | console.error("Something went wrong fetching OpenAPI:", error); 52 | openAPI = ""; 53 | } 54 | 55 | res.status(200).json({ 56 | status: "success", 57 | plugin: JSON.stringify(manifest), 58 | openapi: JSON.stringify(openAPI), 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /components/ui/DeveloperPluginCard.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/alt-text */ 2 | /* eslint-disable @next/next/no-img-element */ 3 | 4 | import { ArrowRight } from "lucide-react"; 5 | import Link from "next/link"; 6 | 7 | export default function DevloperPluginCard({ 8 | name, 9 | description, 10 | authorGithubUsername, 11 | githubUrl, 12 | }: { 13 | name: string; 14 | description: string; 15 | authorGithubUsername: string; 16 | githubUrl: string; 17 | }) { 18 | return ( 19 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /pages/api/og.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/alt-text */ 2 | /* eslint-disable @next/next/no-img-element */ 3 | import { ImageResponse } from "@vercel/og"; 4 | import { NextRequest } from "next/server"; 5 | 6 | export const config = { 7 | runtime: "edge", 8 | }; 9 | 10 | export default async function handler(request: NextRequest) { 11 | const { searchParams } = request.nextUrl; 12 | const name = searchParams.get("name"); 13 | const logo = searchParams.get("logo"); 14 | const description = searchParams.get("description"); 15 | 16 | if (!name || !logo || !description) { 17 | return new ImageResponse(<>Invalid Plugin, { 18 | width: 1200, 19 | height: 630, 20 | }); 21 | } 22 | 23 | return new ImageResponse( 24 | ( 25 |
26 |
27 | 33 |

gptplugins.app

34 |

{name}

35 |

{description}

36 |
37 | 38 |
39 | 46 |
47 |
48 | ), 49 | { 50 | width: 1200, 51 | height: 630, 52 | } 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /pages/api/submit.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import prisma from "@/lib/prisma"; 3 | 4 | type Data = { 5 | status: string; 6 | id?: number; 7 | }; 8 | 9 | type Body = { 10 | name: string; 11 | url: string; 12 | type: "hosted" | "open"; 13 | description?: string; 14 | }; 15 | 16 | type Auth = { 17 | type: string; 18 | }; 19 | 20 | type Api = { 21 | type: string; 22 | url: string; 23 | }; 24 | 25 | export type AiPlugin = { 26 | schema_version: string; 27 | name_for_model: string; 28 | name_for_human: string; 29 | description_for_human: string; 30 | description_for_model: string; 31 | api: Api; 32 | auth: Auth; 33 | logo_url: string; 34 | contact_email: string; 35 | legal_info_url: string; 36 | }; 37 | 38 | export default async function handler( 39 | req: NextApiRequest, 40 | res: NextApiResponse 41 | ) { 42 | const body = req.body as Body; 43 | const url = body.url; 44 | const name = body.name; 45 | const type = body.type; 46 | 47 | if (type === "hosted") { 48 | try { 49 | const result = await fetch(url); 50 | const data: AiPlugin = await result.json(); 51 | 52 | const description = data.description_for_human; 53 | const logo = data.logo_url; 54 | const plugin = await prisma.plugin.create({ 55 | data: { 56 | name: name, 57 | description: description, 58 | logo: logo, 59 | url: url, 60 | approved: false, 61 | }, 62 | }); 63 | res.status(200).json({ status: "success", id: plugin.id }); 64 | } catch (e) { 65 | console.log(e); 66 | res.status(500).json({ status: "error" }); 67 | return; 68 | } 69 | } else { 70 | try { 71 | const description = body?.description || ""; 72 | const plugin = await prisma.oSPlugin.create({ 73 | data: { 74 | name: name, 75 | description: description, 76 | githubURL: url, 77 | approved: false, 78 | }, 79 | }); 80 | res.status(200).json({ status: "success", id: plugin.id }); 81 | } catch (e) { 82 | console.log(e); 83 | res.status(500).json({ status: "error" }); 84 | return; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /components/ui/ScrollArea.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | <> 31 | 41 | 42 | 43 | 54 | 55 | 56 | 57 | )); 58 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 59 | 60 | export { ScrollArea, ScrollBar }; 61 | -------------------------------------------------------------------------------- /app/about/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Footer from "@/components/Footer"; 4 | import Navbar from "@/components/Navbar"; 5 | import Image from "next/image"; 6 | 7 | export default function About() { 8 | return ( 9 |
10 | 11 |
12 | {/* Hero section */} 13 |
14 |
49 |
50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | interface NavigationItem { 4 | name: string; 5 | href: string; 6 | icon: (props: SVGProps) => JSX.Element; 7 | } 8 | 9 | const navigation: NavigationItem[] = [ 10 | { 11 | name: "Twitter", 12 | href: "https://twitter.com/krishnerkar", 13 | icon: (props) => ( 14 | 15 | 16 | 17 | ), 18 | }, 19 | { 20 | name: "GitHub", 21 | href: "https://github.com/krishnerkar/gpt-plugins", 22 | icon: (props) => ( 23 | 24 | 29 | 30 | ), 31 | }, 32 | ]; 33 | 34 | export default function Footer() { 35 | const d = new Date(); 36 | let year = d.getFullYear(); 37 | return ( 38 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Dialog } from "@headlessui/react"; 4 | import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline"; 5 | import Image from "next/image"; 6 | import Link from "next/link"; 7 | import { useState } from "react"; 8 | 9 | const navigation = [ 10 | { name: "About", href: "/about" }, 11 | { name: "Chat", href: "/chat" }, 12 | { name: "API", href: "/api" }, 13 | ]; 14 | 15 | export default function Navbar() { 16 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false); 17 | 18 | return ( 19 |
20 | 69 | 75 |
76 | 77 |
78 | 79 | Gpt plugins 80 | logo 87 | 88 | 96 |
97 |
98 |
99 |
100 | {navigation.map((item) => ( 101 | 106 | {item.name} 107 | 108 | ))} 109 |
110 |
111 | 115 | Submit a plugin 116 | 117 |
118 |
119 |
120 |
121 |
122 |
123 | ); 124 | } 125 | -------------------------------------------------------------------------------- /components/ui/PluginDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PluginDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const PluginDialog = PluginDialogPrimitive.Root; 9 | 10 | const PluginDialogTrigger = PluginDialogPrimitive.Trigger; 11 | 12 | const PluginDialogPortal = ({ 13 | className, 14 | children, 15 | ...props 16 | }: PluginDialogPrimitive.AlertDialogPortalProps) => ( 17 | 18 |
19 | {children} 20 |
21 |
22 | ); 23 | PluginDialogPortal.displayName = PluginDialogPrimitive.Portal.displayName; 24 | 25 | const PluginDialogOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, children, ...props }, ref) => ( 29 | 37 | )); 38 | PluginDialogOverlay.displayName = PluginDialogPrimitive.Overlay.displayName; 39 | 40 | const PluginDialogContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 45 | 46 | 55 | 56 | )); 57 | PluginDialogContent.displayName = PluginDialogPrimitive.Content.displayName; 58 | 59 | const PluginDialogHeader = ({ 60 | className, 61 | ...props 62 | }: React.HTMLAttributes) => ( 63 |
70 | ); 71 | PluginDialogHeader.displayName = "PluginDialogHeader"; 72 | 73 | const PluginDialogFooter = ({ 74 | className, 75 | ...props 76 | }: React.HTMLAttributes) => ( 77 |
84 | ); 85 | PluginDialogFooter.displayName = "PluginDialogFooter"; 86 | 87 | const PluginDialogTitle = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => ( 91 | 100 | )); 101 | PluginDialogTitle.displayName = PluginDialogPrimitive.Title.displayName; 102 | 103 | const PluginDialogDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )); 113 | PluginDialogDescription.displayName = 114 | PluginDialogPrimitive.Description.displayName; 115 | 116 | const PluginDialogAction = React.forwardRef< 117 | React.ElementRef, 118 | React.ComponentPropsWithoutRef 119 | >(({ className, ...props }, ref) => ( 120 | 128 | )); 129 | PluginDialogAction.displayName = PluginDialogPrimitive.Action.displayName; 130 | 131 | const PluginDialogCancel = React.forwardRef< 132 | React.ElementRef, 133 | React.ComponentPropsWithoutRef 134 | >(({ className, ...props }, ref) => ( 135 | 143 | )); 144 | PluginDialogCancel.displayName = PluginDialogPrimitive.Cancel.displayName; 145 | 146 | export { 147 | PluginDialog, 148 | PluginDialogTrigger, 149 | PluginDialogContent, 150 | PluginDialogHeader, 151 | PluginDialogFooter, 152 | PluginDialogTitle, 153 | PluginDialogDescription, 154 | PluginDialogAction, 155 | PluginDialogCancel, 156 | }; 157 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @next/next/no-img-element */ 2 | "use client"; 3 | 4 | import Footer from "@/components/Footer"; 5 | import Navbar from "@/components/Navbar"; 6 | import PluginCard from "@/components/ui/PluginCard"; 7 | import PluginCardSkeleton from "@/components/ui/PluginCardSkeleton"; 8 | import { SimplePlugin } from "@/pages/api/getPlugins"; 9 | import { useEffect, useState } from "react"; 10 | import { Toaster, toast } from "sonner"; 11 | import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; 12 | import debounce from "@/lib/debounce"; 13 | import DevloperPluginCard from "@/components/ui/DeveloperPluginCard"; 14 | import { SimpleOSPlugin } from "@/pages/api/getOSPlugins"; 15 | 16 | export default function HomePage() { 17 | const [data, setData] = useState(); 18 | const [oSPlugins, setOSPlugins] = useState([]); 19 | const [loading, setLoading] = useState(false); 20 | 21 | const [searchResults, setSearchResults] = useState< 22 | SimplePlugin[] | SimpleOSPlugin[] 23 | >([]); 24 | const [searchLoading, setSearchLoading] = useState(false); 25 | 26 | const [searchQuery, setSearchQuery] = useState(""); 27 | 28 | const [tabs, setTabs] = useState([ 29 | { 30 | name: "Published Plugins", 31 | href: "#", 32 | current: true, 33 | id: 0, 34 | }, 35 | { name: "Open Source Plugins", href: "#", current: false, id: 1 }, 36 | ]); 37 | 38 | const [activeTab, setActiveTab] = useState(0); 39 | 40 | function handleTabClick(index: number) { 41 | setTabs((prevTabs) => 42 | prevTabs.map((tab, idx) => ({ ...tab, current: idx === index })) 43 | ); 44 | setActiveTab(index); 45 | } 46 | 47 | function classNames(...classes: string[]) { 48 | return classes.filter(Boolean).join(" "); 49 | } 50 | 51 | const fetchSearchResults = async (query: string) => { 52 | if (query.trim() === "") { 53 | setSearchResults([]); 54 | return; 55 | } 56 | setSearchLoading(true); 57 | const response = await fetch( 58 | `/api/search?q=${query}&type=${activeTab == 0 ? "hosted" : "open"}` 59 | ); 60 | const data = await response.json(); 61 | setSearchResults(data); 62 | setSearchLoading(false); 63 | }; 64 | 65 | const handleInputChange = debounce((e) => { 66 | setSearchQuery(e.target.value); 67 | setSearchLoading(true); 68 | fetchSearchResults(e.target.value); 69 | }, 300); 70 | 71 | useEffect(() => { 72 | if (!data) { 73 | setLoading(true); 74 | fetch("/api/getPlugins").then((res) => { 75 | if (res.ok) { 76 | res.json().then((data) => { 77 | setData(data); 78 | }); 79 | } else { 80 | toast.error("Something went wrong"); 81 | } 82 | }); 83 | 84 | fetch("/api/getOSPlugins").then((res) => { 85 | if (res.ok) { 86 | res.json().then((data) => { 87 | setOSPlugins(data); 88 | }); 89 | } else { 90 | toast.error("Something went wrong"); 91 | } 92 | setLoading(false); 93 | }); 94 | } 95 | }, [data]); 96 | 97 | return ( 98 | <> 99 | 100 | 101 |
102 |
103 | 106 |
224 | 225 |