├── .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 |
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 |
4 |
5 |
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 |
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 |
18 |
19 |
20 |
21 | We’re building an all in one platform for ChatGPT Plugins
22 |
23 |
24 |
25 | GPT Plugins makes it extremely easy for you to find the best
26 | plugins for ChatGPT. Many more features are coming soon!
27 | Follow{" "}
28 |
32 | here
33 | {" "}
34 | for updates.
35 |
36 |
37 |
44 |
45 |
46 |
47 |
48 |
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 |
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 |
138 |
139 | {!loading && (
140 |
222 | )}
223 |
224 |
225 |
226 | >
227 | );
228 | }
229 |
--------------------------------------------------------------------------------
/app/submit/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 Link from "next/link";
7 | import { useState, FormEvent } from "react";
8 | import { Toaster, toast } from "sonner";
9 |
10 | export default function Add() {
11 | const [name, setName] = useState("");
12 | const [url, setUrl] = useState("");
13 | const [loading, setLoading] = useState(false);
14 | const [description, setDescription] = useState("");
15 |
16 | const [tabs, setTabs] = useState([
17 | {
18 | name: "Hosted Plugin",
19 | href: "#",
20 | current: true,
21 | id: 0,
22 | },
23 | { name: "Open Source Plugin", href: "#", current: false, id: 1 },
24 | ]);
25 |
26 | const [activeTab, setActiveTab] = useState(0);
27 |
28 | function handleTabClick(index: number) {
29 | setTabs((prevTabs) =>
30 | prevTabs.map((tab, idx) => ({ ...tab, current: idx === index }))
31 | );
32 | setActiveTab(index);
33 | }
34 |
35 | function classNames(...classes: string[]) {
36 | return classes.filter(Boolean).join(" ");
37 | }
38 |
39 | const handleSubmit = async (e: FormEvent) => {
40 | e.preventDefault();
41 | setLoading(true);
42 | const response = await fetch("/api/submit/", {
43 | method: "POST",
44 | headers: {
45 | "Content-Type": "application/json",
46 | },
47 | body: JSON.stringify({
48 | name,
49 | url,
50 | description,
51 | type: activeTab == 0 ? "hosted" : "open",
52 | }),
53 | });
54 |
55 | if (response.status === 200) {
56 | toast.success(`Plugin submitted successfully!`);
57 | setUrl("");
58 | setName("");
59 | setDescription("");
60 | } else {
61 | toast.error("Error: Failed to add plugin");
62 | }
63 | setLoading(false);
64 | };
65 |
66 | return (
67 |
215 | );
216 | }
217 |
--------------------------------------------------------------------------------
/app/@modal/(..)plugin/[name]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | PluginDialog,
5 | PluginDialogCancel,
6 | PluginDialogContent,
7 | PluginDialogDescription,
8 | PluginDialogHeader,
9 | } from "@/components/ui/PluginDialog";
10 | import { useCallback, useEffect, useState } from "react";
11 | import { SimplePlugin } from "@/pages/api/getPlugins";
12 | import { Result } from "@/pages/api/getByName";
13 | import { ScrollArea } from "@/components/ui/ScrollArea";
14 | import { useRouter } from "next/navigation";
15 | import { Clipboard, Maximize2 } from "lucide-react";
16 |
17 | import copy from "clipboard-copy";
18 | import { Toaster, toast } from "sonner";
19 |
20 | export default function PhotoModal({ params }: { params: { name: string } }) {
21 | const [data, setData] = useState();
22 | const [loading, setLoading] = useState(false);
23 | const [activeContent, setActiveContent] = useState("");
24 |
25 | const router = useRouter();
26 |
27 | const name = params.name;
28 |
29 | const closeModal = useCallback(() => {
30 | router.back();
31 | }, [router]);
32 |
33 | const [loadingJSON, setLoadingJSON] = useState(false);
34 |
35 | const handleImageError = (e: React.SyntheticEvent) => {
36 | const fallbackImageUrl = "/fallback.svg";
37 | e.currentTarget.src = fallbackImageUrl;
38 | };
39 |
40 | const [tabs, setTabs] = useState([
41 | {
42 | name: "Plugin JSON",
43 | href: "#",
44 | current: true,
45 | content: "",
46 | },
47 | { name: "OpenAPI", href: "#", current: false, content: "" },
48 | ]);
49 |
50 | useEffect(() => {
51 | if (!data) return;
52 | setLoadingJSON(true);
53 | fetch(`/api/fetch`, {
54 | method: "POST",
55 | headers: {
56 | "Content-Type": "application/json",
57 | },
58 | body: JSON.stringify({
59 | url: data.url,
60 | }),
61 | }).then((res) => {
62 | res.json().then((data: { plugin: string; openapi: string }) => {
63 | const plugin = JSON.parse(data.plugin);
64 | const openapi = JSON.parse(data.openapi);
65 |
66 | setTabs((prevTabs) => [
67 | { ...prevTabs[0], content: plugin },
68 | { ...prevTabs[1], content: openapi },
69 | ]);
70 |
71 | setLoadingJSON(false);
72 | });
73 | });
74 | }, [data]);
75 |
76 | useEffect(() => {
77 | const activeTab = tabs.find((tab) => tab.current);
78 | setActiveContent(activeTab?.content || tabs[0].content);
79 | }, [tabs]);
80 |
81 | function handleTabClick(index: number) {
82 | setTabs((prevTabs) =>
83 | prevTabs.map((tab, idx) => ({ ...tab, current: idx === index }))
84 | );
85 | }
86 |
87 | function classNames(...classes: string[]) {
88 | return classes.filter(Boolean).join(" ");
89 | }
90 |
91 | useEffect(() => {
92 | if (!data) {
93 | setLoading(true);
94 | fetch(`/api/getByName?name=${name}`).then((res) => {
95 | if (res.ok) {
96 | res.json().then((data: Result) => {
97 | if (data.error) {
98 | return;
99 | }
100 | if (!data.data) return;
101 | setData(data.data);
102 | });
103 | } else {
104 | res.json().then((data: Result) => {
105 | if (data.error) {
106 | return;
107 | }
108 | });
109 | }
110 | setLoading(false);
111 | });
112 | }
113 | }, [data, name]);
114 |
115 | return (
116 | <>
117 |
118 |
119 |
120 |
121 |
122 |
123 | {loading ? (
124 |
125 | ) : (
126 |
133 | )}
134 |
135 | {loading ? (
136 |
137 | ) : (
138 |
139 | {data?.name}
140 |
141 | )}
142 |
143 |
144 | {loading ? (
145 | <>
146 |
147 |
148 | >
149 | ) : (
150 |
151 | {data?.description}
152 |
153 | )}
154 |
155 | {!loading && (
156 |
157 |
162 | {
164 | void copy(data?.url || "").then(() => {
165 | toast.success("Copied plugin URL to clipboard!");
166 | });
167 | }}
168 | style={{
169 | marginLeft: "-45px",
170 | }}
171 | className="disabled:opacity-50 disabled:pointer-events-none flex justify-center rounded-md bg-indigo-600 px-2 h-full py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
172 | >
173 |
174 |
175 |
176 | )}
177 |
178 |
179 | {!loading && (
180 |
211 | )}
212 |
213 |
214 | {
216 | window.location.reload();
217 | }}
218 | className="bg-indigo-600 p-3 rounded-full hover:bg-indigo-800 absolute right-6 top-4"
219 | >
220 |
221 |
222 |
223 |
224 | Close
225 |
226 |
227 |
228 |
229 | >
230 | );
231 | }
232 |
--------------------------------------------------------------------------------
/components/PluginContent.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/alt-text */
2 | /* eslint-disable @next/next/no-img-element */
3 | "use client";
4 |
5 | import Navbar from "@/components/Navbar";
6 | import { SimplePlugin } from "@/pages/api/getPlugins";
7 | import { Result } from "@/pages/api/getByName";
8 | import { useEffect, useState } from "react";
9 | import { toast, Toaster } from "sonner";
10 | import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
11 | import Link from "next/link";
12 | import { Clipboard } from "lucide-react";
13 | import copy from "clipboard-copy";
14 | import Footer from "@/components/Footer";
15 | import { ScrollArea } from "@/components/ui/ScrollArea";
16 | import { Metadata, ResolvingMetadata } from "next";
17 |
18 | export default function PluginContent({ name }: { name: string }) {
19 | const [data, setData] = useState();
20 | const [loading, setLoading] = useState(false);
21 | const [activeContent, setActiveContent] = useState("");
22 |
23 | const [loadingJSON, setLoadingJSON] = useState(false);
24 |
25 | const handleImageError = (e: React.SyntheticEvent) => {
26 | const fallbackImageUrl = "/fallback.svg";
27 | e.currentTarget.src = fallbackImageUrl;
28 | };
29 |
30 | const [tabs, setTabs] = useState([
31 | {
32 | name: "Plugin JSON",
33 | href: "#",
34 | current: true,
35 | content: "",
36 | },
37 | { name: "OpenAPI", href: "#", current: false, content: "" },
38 | ]);
39 |
40 | useEffect(() => {
41 | if (!data) return;
42 | setLoadingJSON(true);
43 | fetch(`/api/fetch`, {
44 | method: "POST",
45 | headers: {
46 | "Content-Type": "application/json",
47 | },
48 | body: JSON.stringify({
49 | url: data.url,
50 | }),
51 | }).then((res) => {
52 | res.json().then((data: { plugin: string; openapi: string }) => {
53 | const plugin = JSON.parse(data.plugin);
54 | const openapi = JSON.parse(data.openapi);
55 |
56 | setTabs((prevTabs) => [
57 | { ...prevTabs[0], content: plugin },
58 | { ...prevTabs[1], content: openapi },
59 | ]);
60 |
61 | setLoadingJSON(false);
62 | });
63 | });
64 | }, [data]);
65 |
66 | useEffect(() => {
67 | const activeTab = tabs.find((tab) => tab.current);
68 | setActiveContent(activeTab?.content || tabs[0].content);
69 | }, [tabs]);
70 |
71 | function handleTabClick(index: number) {
72 | setTabs((prevTabs) =>
73 | prevTabs.map((tab, idx) => ({ ...tab, current: idx === index }))
74 | );
75 | }
76 |
77 | function classNames(...classes: string[]) {
78 | return classes.filter(Boolean).join(" ");
79 | }
80 |
81 | useEffect(() => {
82 | if (!data) {
83 | setLoading(true);
84 | fetch(`/api/getByName?name=${name}`).then((res) => {
85 | if (res.ok) {
86 | res.json().then((data: Result) => {
87 | if (data.error) {
88 | toast.error(data.error);
89 | return;
90 | }
91 | if (!data.data) return;
92 | setData(data.data);
93 | });
94 | } else {
95 | res.json().then((data: Result) => {
96 | if (data.error) {
97 | toast.error(data.error);
98 | return;
99 | } else {
100 | toast.error("Something went wrong");
101 | }
102 | });
103 | }
104 | setLoading(false);
105 | });
106 | }
107 | }, [data, name]);
108 |
109 | return (
110 | <>
111 |
112 |
113 |
114 |
115 |
116 |
120 |
124 | Back
125 |
126 |
127 |
128 |
129 |
130 |
131 |
135 | Plugins
136 |
137 |
138 |
139 |
140 |
141 |
154 |
155 |
156 |
157 |
158 |
159 | {loading ? (
160 |
161 | ) : (
162 |
168 | )}
169 |
170 | {loading ? (
171 |
172 | ) : (
173 |
174 | {data?.name}
175 |
176 | )}
177 |
178 |
179 | {loading ? (
180 | <>
181 |
182 |
183 | >
184 | ) : (
185 |
186 | {data?.description}
187 |
188 | )}
189 |
190 | {!loading && (
191 |
192 |
197 | {
199 | void copy(data?.url || "").then(() => {
200 | toast.success("Copied plugin URL to clipboard!");
201 | });
202 | }}
203 | style={{
204 | marginLeft: "-45px",
205 | }}
206 | className="disabled:opacity-50 disabled:pointer-events-none flex justify-center rounded-md bg-indigo-600 px-2 h-full py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
207 | >
208 |
209 |
210 |
211 | )}
212 |
213 |
214 | {!loading && (
215 |
244 | )}
245 |
246 |
247 |
248 | >
249 | );
250 | }
251 |
--------------------------------------------------------------------------------