├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── .idea ├── .gitignore ├── DevToolboxWeb.iml ├── inspectionProfiles │ └── Project_Default.xml ├── jsLinters │ └── eslint.xml ├── modules.xml ├── prettier.xml └── vcs.xml ├── Dockerfile ├── LICENSE.md ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── devtoolbox-screenshot.png └── devtoolbox_logo.png ├── src ├── actions │ ├── stripe.ts │ └── user.ts ├── app │ ├── api │ │ ├── history │ │ │ └── route.ts │ │ └── webhooks │ │ │ └── stripe │ │ │ └── route.ts │ ├── components │ │ └── common │ │ │ ├── Button.tsx │ │ │ ├── FormatedJsonOutput.tsx │ │ │ ├── FormatedJsonOutputV2.tsx │ │ │ ├── FormattedMarkdownOutput.tsx │ │ │ ├── ReadOnlyTextArea.tsx │ │ │ ├── Selector.tsx │ │ │ ├── TextArea.tsx │ │ │ └── ToolList.tsx │ ├── favicon.ico │ ├── globals.css │ ├── hooks │ │ └── useDebounce.ts │ ├── layout.tsx │ ├── page.tsx │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ ├── sign-up │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── successful-purchase │ │ ├── SuccessfulPurchase.tsx │ │ └── page.tsx │ └── tools │ │ ├── ascii-converter │ │ ├── AsciiConverterComponent.tsx │ │ └── page.tsx │ │ ├── base64encoder │ │ ├── Base64EncoderClientComponent.tsx │ │ └── page.tsx │ │ ├── base64imageencoder │ │ ├── Base64ImageEncoderComponent.tsx │ │ └── page.tsx │ │ ├── character-and-word-counter │ │ ├── CharacterAndWordCounterComponent.tsx │ │ └── page.tsx │ │ ├── clipboard-formatter │ │ ├── ClipFormatterComponent.tsx │ │ └── page.tsx │ │ ├── color-converter │ │ ├── ColorConverterComponent.tsx │ │ └── page.tsx │ │ ├── css-unit-converter │ │ ├── page.tsx │ │ └── unitConverter.tsx │ │ ├── diff-viewer │ │ ├── DiffViewerComponent.tsx │ │ └── page.tsx │ │ ├── hash-generator │ │ ├── HashGeneratorComponent.tsx │ │ └── page.tsx │ │ ├── history │ │ └── page.tsx │ │ ├── json-validator │ │ ├── JsonValidatorComponent.tsx │ │ └── page.tsx │ │ ├── jwt-viewer │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── line-sort-and-dedupe │ │ ├── LineSortAndDedupeComponent.tsx │ │ └── page.tsx │ │ ├── markdown-editor │ │ ├── MarkdownEditorComponent.tsx │ │ └── page.tsx │ │ ├── qrcode-generator │ │ ├── QrCodeGeneratorComponent.tsx │ │ └── page.tsx │ │ ├── regex-checker │ │ ├── RegexCheckerComponent.tsx │ │ └── page.tsx │ │ ├── string-converter │ │ ├── StringConverterComponent.tsx │ │ └── page.tsx │ │ ├── unix-time-converter │ │ ├── UnixTimeConverterComponent.tsx │ │ └── page.tsx │ │ ├── url-decoder │ │ ├── UrlDecoderComponent.tsx │ │ └── page.tsx │ │ ├── url-encoder │ │ ├── UrlEncoderComponent.tsx │ │ └── page.tsx │ │ ├── url-parser │ │ ├── UrlParserComponent.tsx │ │ └── page.tsx │ │ └── uuid-generator │ │ ├── UuidGeneratorComponent.tsx │ │ └── page.tsx ├── db │ └── prisma.ts ├── lib │ └── stripe.ts ├── middleware.ts └── utils │ ├── clientUtils.ts │ └── findNearestNumber.ts ├── tailwind.config.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 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 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/DevToolboxWeb.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/jsLinters/eslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/prettier.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:lts-bullseye-slim AS deps 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN apt update 5 | WORKDIR /app 6 | 7 | COPY package.json package-lock.json ./ 8 | RUN npm install 9 | 10 | # Rebuild the source code only when needed 11 | FROM node:lts-bullseye-slim AS builder 12 | WORKDIR /app 13 | COPY --from=deps /app/node_modules ./node_modules 14 | COPY . . 15 | 16 | # Next.js collects completely anonymous telemetry data about general usage. 17 | # Learn more here: https://nextjs.org/telemetry 18 | # Uncomment the following line in case you want to disable telemetry during the build. 19 | # ENV NEXT_TELEMETRY_DISABLED 1 20 | 21 | RUN npx prisma generate 22 | RUN npm run build 23 | 24 | # Production image, copy all the files and run next 25 | FROM node:lts-bullseye-slim AS runner 26 | WORKDIR /app 27 | 28 | ENV NODE_ENV production 29 | # Uncomment the following line in case you want to disable telemetry during runtime. 30 | # ENV NEXT_TELEMETRY_DISABLED 1 31 | 32 | RUN addgroup --system --gid 1001 nodejs 33 | RUN adduser --system --uid 1001 nextjs 34 | 35 | # You only need to copy next.config.js if you are NOT using the default configuration 36 | # COPY --from=builder /app/next.config.js ./ 37 | COPY --from=builder /app/public ./public 38 | COPY --from=builder /app/package.json ./package.json 39 | 40 | # Automatically leverage output traces to reduce image size 41 | # https://nextjs.org/docs/advanced-features/output-file-tracing 42 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 43 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 44 | 45 | USER nextjs 46 | 47 | EXPOSE 3000 48 | 49 | ENV PORT 3000 50 | 51 | CMD ["node", "server.js"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dohyun Kim 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | ### 1. Obtain Your Clerk Secret Key or API Key 6 | 7 | - Go to the Clerk [Dashboard](https://dashboard.clerk.com/sign-in). 8 | - Log in with your credentials. 9 | - Navigate to your instance or project settings. 10 | - Look for the section where API keys or secrets are listed. You should find your Secret Key or API Key there. 11 | 12 | ### 2. Create a .env.local File 13 | 14 | - Create the **.env.local** File: In the root directory of your project, create a file named .env.local 15 | - Add Your Keys: Open the **.env.local** file and add your Clerk keys like so: 16 | 17 | ```bash 18 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 19 | CLERK_SECRET_KEY= 20 | ``` 21 | ### 3. Run Your Development Server 22 | 23 | ```bash 24 | npm run dev 25 | # or 26 | yarn dev 27 | # or 28 | pnpm dev 29 | # or 30 | bun dev 31 | ``` 32 | 33 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 34 | 35 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 36 | 37 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 38 | 39 | ## Learn More 40 | 41 | To learn more about Next.js, take a look at the following resources: 42 | 43 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 44 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 45 | 46 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 47 | 48 | ## Deploy on Vercel 49 | 50 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 51 | 52 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 53 | 54 | ## Self hosting with docker 55 | 56 | Although the project is not yet available on Docker Hub, you can self-host it by building the Docker image yourself. Follow these steps to get started: 57 | 58 | **Step 1** : install the docker engine ([how to install docker](https://docs.docker.com/engine/install/)) 59 | 60 | **Step 2** : clone the repository 61 | 62 | **Step 3** : build the container 63 | 64 | ```bash 65 | docker build . -t devoolboxweb 66 | ``` 67 | **Step 4** get api key on [Clerk](https://dashboard.clerk.com/sign-in) 68 | **Step 5** : run the docker with needed variables : 69 | 70 | ```bash 71 | docker container run \ 72 | --name devoolboxweb \ 73 | -p 3000:3000 \ 74 | -e NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_key \ 75 | -e CLERK_SECRET_KEY=your_secret_key \ 76 | devoolboxweb 77 | ``` 78 | 79 | Note : without the vars `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` , the container will throw error code 500. -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | output: 'standalone', 4 | experimental: { 5 | serverActions: true, 6 | }, 7 | }; 8 | 9 | module.exports = nextConfig; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devtoolbox", 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 | "postinstall": "prisma generate" 11 | }, 12 | "dependencies": { 13 | "@clerk/nextjs": "^4.25.7", 14 | "@headlessui/react": "^1.7.17", 15 | "@heroicons/react": "^2.0.18", 16 | "@prisma/client": "^5.11.0", 17 | "@types/dompurify": "^3.0.5", 18 | "class-variance-authority": "^0.7.0", 19 | "crypto-js": "^4.2.0", 20 | "dompurify": "^3.0.6", 21 | "github-markdown-css": "^5.4.0", 22 | "jsonpath-plus": "^7.2.0", 23 | "jwt-decode": "^4.0.0", 24 | "marked": "^10.0.0", 25 | "next": "13.5.6", 26 | "react": "^18", 27 | "react-dom": "^18", 28 | "react-json-view": "1.21.3", 29 | "react-qr-code": "^2.0.12", 30 | "stripe": "^14.1.0", 31 | "uuid": "^9.0.1", 32 | "zod": "^3.22.4" 33 | }, 34 | "devDependencies": { 35 | "@types/crypto-js": "^4.1.3", 36 | "@types/jsonpath": "^0.2.2", 37 | "@types/node": "^20", 38 | "@types/react": "^18", 39 | "@types/react-dom": "^18", 40 | "@types/uuid": "^9.0.7", 41 | "autoprefixer": "^10", 42 | "eslint": "^8", 43 | "eslint-config-next": "13.5.6", 44 | "postcss": "^8", 45 | "prisma": "^5.5.0", 46 | "react-diff-viewer-continued": "^3.3.1", 47 | "tailwindcss": "^3", 48 | "typescript": "^5" 49 | }, 50 | "overrides": { 51 | "react-json-view": { 52 | "react": "$react", 53 | "react-dom": "$react-dom" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("POSTGRES_PRISMA_URL") // uses connection pooling 8 | directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection 9 | } 10 | 11 | model History { 12 | id String @id @default(cuid()) 13 | createdAt DateTime @default(now()) 14 | toolType ToolType 15 | metadata Json 16 | userId String 17 | } 18 | 19 | model Subscriptions { 20 | stripeCustomerId String @id 21 | clerkUserId String 22 | subscriptionStatus SubscriptionStatus 23 | } 24 | 25 | enum ToolType { 26 | Base64Encoder 27 | Base64ImageEncoder 28 | CharacterAndWordCounter 29 | ColorConverter 30 | DiffViewer 31 | HashGenerator 32 | JsonValidator 33 | LineSortAndDedupe 34 | RegexChecker 35 | StringConverter 36 | UnixTimeConverter 37 | UrlParser 38 | QrCodeGenerator 39 | AsciiConverter 40 | JwtViewer 41 | } 42 | 43 | enum SubscriptionStatus { 44 | ACTIVE 45 | INACTIVE 46 | } 47 | -------------------------------------------------------------------------------- /public/devtoolbox-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YourAverageTechBro/DevToolboxWeb/d5e14628db174c0dc1a9a4249efe85536be79067/public/devtoolbox-screenshot.png -------------------------------------------------------------------------------- /public/devtoolbox_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YourAverageTechBro/DevToolboxWeb/d5e14628db174c0dc1a9a4249efe85536be79067/public/devtoolbox_logo.png -------------------------------------------------------------------------------- /src/actions/stripe.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import type { Stripe } from "stripe"; 4 | 5 | import { redirect } from "next/navigation"; 6 | import { headers } from "next/headers"; 7 | import { stripe } from "@/lib/stripe"; 8 | import prisma from "@/db/prisma"; 9 | 10 | export async function createYearlyCheckoutSession( 11 | data: FormData 12 | ): Promise { 13 | const userId = data.get("userId") as string; 14 | const checkoutSession: Stripe.Checkout.Session = 15 | await stripe.checkout.sessions.create({ 16 | allow_promotion_codes: true, 17 | mode: "subscription", 18 | metadata: { 19 | userId, 20 | }, 21 | line_items: [ 22 | { 23 | price: process.env.NEXT_PUBLIC_YEARLY_PRODUCT_ID, 24 | // For metered billing, do not pass quantity 25 | quantity: 1, 26 | }, 27 | ], 28 | success_url: `${headers().get( 29 | "origin" 30 | )}/successful-purchase?session_id={CHECKOUT_SESSION_ID}`, 31 | cancel_url: `${headers().get("origin")}/tools/history`, 32 | }); 33 | 34 | redirect(checkoutSession.url as string); 35 | } 36 | export async function createLifetimeCheckoutSession( 37 | data: FormData 38 | ): Promise { 39 | const userId = data.get("userId") as string; 40 | const checkoutSession: Stripe.Checkout.Session = 41 | await stripe.checkout.sessions.create({ 42 | allow_promotion_codes: true, 43 | mode: "payment", 44 | metadata: { 45 | userId, 46 | }, 47 | line_items: [ 48 | { 49 | price: process.env.NEXT_PUBLIC_LIFETIME_PRODUCT_ID, 50 | // For metered billing, do not pass quantity 51 | quantity: 1, 52 | }, 53 | ], 54 | success_url: `${headers().get( 55 | "origin" 56 | )}/successful-purchase?session_id={CHECKOUT_SESSION_ID}`, 57 | cancel_url: `${headers().get("origin")}/tools/history`, 58 | }); 59 | 60 | redirect(checkoutSession.url as string); 61 | } 62 | 63 | export async function redirectToCustomerPortal(data: FormData) { 64 | const userId = data.get("userId") as string; 65 | const subscription = await prisma.subscriptions.findFirst({ 66 | where: { 67 | clerkUserId: userId, 68 | }, 69 | }); 70 | if (!subscription) { 71 | throw Error("Subscription not found"); 72 | } 73 | const stripeCustomerId = subscription.stripeCustomerId; 74 | const session = await stripe.billingPortal.sessions.create({ 75 | customer: stripeCustomerId, 76 | return_url: "https://devtoolbox.co/tools/history", 77 | }); 78 | 79 | redirect(session.url as string); 80 | } 81 | -------------------------------------------------------------------------------- /src/actions/user.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import prisma from "@/db/prisma"; 4 | import { currentUser } from "@clerk/nextjs"; 5 | import { SubscriptionStatus } from "@prisma/client"; 6 | 7 | export const getUserAndSubscriptionState = async () => { 8 | const user = await currentUser(); 9 | let isProUser = false; 10 | if (user) { 11 | const subscriptions = await prisma.subscriptions.findMany({ 12 | where: { 13 | clerkUserId: user.id, 14 | }, 15 | }); 16 | if ( 17 | subscriptions.length !== 0 && 18 | subscriptions[0].subscriptionStatus === SubscriptionStatus.ACTIVE 19 | ) { 20 | isProUser = true; 21 | } 22 | } 23 | return { 24 | user, 25 | isProUser, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/app/api/history/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { z } from "zod"; 3 | import { ToolType } from "@prisma/client"; 4 | import prisma from "@/db/prisma"; 5 | 6 | const postRequestValidator = z.object({ 7 | toolType: z.nativeEnum(ToolType), 8 | metadata: z.string(), 9 | userId: z.string(), 10 | }); 11 | export const POST = async (req: NextRequest) => { 12 | try { 13 | const body = await req.json(); 14 | 15 | const { userId, toolType, metadata } = postRequestValidator.parse(body); 16 | const parsedMetadata = JSON.parse(metadata); 17 | await prisma.history.create({ 18 | data: { 19 | userId, 20 | toolType, 21 | metadata: parsedMetadata, 22 | }, 23 | }); 24 | return NextResponse.json( 25 | { status: "success", message: "Successfully saved history" }, 26 | { status: 200 } 27 | ); 28 | } catch (error) { 29 | return NextResponse.json( 30 | { status: "error", message: "Invalid request body" }, 31 | { status: 422 } 32 | ); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/app/api/webhooks/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import type { Stripe } from "stripe"; 2 | import prisma from "@/db/prisma"; 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { stripe } from "@/lib/stripe"; 5 | import { SubscriptionStatus } from ".prisma/client"; 6 | 7 | export const POST = async (req: NextRequest) => { 8 | console.log("Starting endpoint", { 9 | path: "webhooks/stripe", 10 | method: "POST", 11 | }); 12 | let event: Stripe.Event; 13 | let text; 14 | try { 15 | text = await req.text(); 16 | console.log("Successfully parsed stripe webhook event", { 17 | text, 18 | }); 19 | event = stripe.webhooks.constructEvent( 20 | text, 21 | req.headers.get("stripe-signature") as string, 22 | process.env.STRIPE_WEBHOOK_SECRET as string 23 | ); 24 | } catch (err) { 25 | const errorMessage = err instanceof Error ? err.message : "Unknown error"; 26 | // On error, log and return the error message. 27 | if (err! instanceof Error) console.log(err); 28 | console.error("Error onendpoint", { 29 | text, 30 | path: "webhooks/stripe", 31 | method: "POST", 32 | error: errorMessage, 33 | }); 34 | return NextResponse.json( 35 | { message: `Webhook Error: ${errorMessage}` }, 36 | { status: 400 } 37 | ); 38 | } 39 | 40 | // Successfully constructed event. 41 | console.log("Successfully parsed stripe webhook event", { 42 | text, 43 | path: "webhooks/stripe", 44 | method: "POST", 45 | event, 46 | }); 47 | 48 | const permittedEvents: string[] = [ 49 | "checkout.session.completed", 50 | "invoice.paid", 51 | "customer.subscription.deleted", 52 | "invoice.payment_failed", 53 | ]; 54 | 55 | if (permittedEvents.includes(event.type)) { 56 | let data; 57 | let stripeCustomerId; 58 | 59 | try { 60 | switch (event.type) { 61 | case "checkout.session.completed": 62 | console.log("Starting to handle checkout.session.completed", { 63 | text, 64 | path: "webhooks/stripe", 65 | method: "POST", 66 | eventType: event.type, 67 | event, 68 | }); 69 | data = event.data.object as Stripe.Checkout.Session; 70 | const { userId } = data.metadata as { userId: string }; 71 | stripeCustomerId = data.customer as string; 72 | await prisma.subscriptions.create({ 73 | data: { 74 | stripeCustomerId, 75 | clerkUserId: userId, 76 | subscriptionStatus: SubscriptionStatus.ACTIVE, 77 | }, 78 | }); 79 | console.log("Completed handling checkout.session.completed", { 80 | text, 81 | path: "webhooks/stripe", 82 | method: "POST", 83 | eventType: event.type, 84 | event, 85 | }); 86 | break; 87 | case "invoice.paid": 88 | console.log("Starting to handle invoice.paid", { 89 | text, 90 | path: "webhooks/stripe", 91 | method: "POST", 92 | eventType: event.type, 93 | event, 94 | }); 95 | data = event.data.object as Stripe.Invoice; 96 | stripeCustomerId = data.customer as string; 97 | await prisma.subscriptions.update({ 98 | where: { 99 | stripeCustomerId, 100 | }, 101 | data: { 102 | subscriptionStatus: SubscriptionStatus.ACTIVE, 103 | }, 104 | }); 105 | console.log("Completed handling invoice.paid", { 106 | text, 107 | path: "webhooks/stripe", 108 | method: "POST", 109 | eventType: event.type, 110 | event, 111 | }); 112 | break; 113 | case "customer.subscription.deleted": 114 | console.log("Starting to handle customer.subscription.deleted", { 115 | text, 116 | path: "webhooks/stripe", 117 | method: "POST", 118 | eventType: event.type, 119 | event, 120 | }); 121 | data = event.data.object as Stripe.Subscription; 122 | stripeCustomerId = data.customer as string; 123 | await prisma.subscriptions.delete({ 124 | where: { 125 | stripeCustomerId, 126 | }, 127 | }); 128 | console.log("Completed handling customer.subscription.deleted", { 129 | text, 130 | path: "webhooks/stripe", 131 | method: "POST", 132 | eventType: event.type, 133 | event, 134 | }); 135 | break; 136 | case "invoice.payment_failed": 137 | console.log("Starting to handle invoice.payment_failed", { 138 | text, 139 | path: "webhooks/stripe", 140 | method: "POST", 141 | eventType: event.type, 142 | event, 143 | }); 144 | data = event.data.object as Stripe.Invoice; 145 | stripeCustomerId = data.customer as string; 146 | await prisma.subscriptions.update({ 147 | where: { 148 | stripeCustomerId, 149 | }, 150 | data: { 151 | subscriptionStatus: SubscriptionStatus.INACTIVE, 152 | }, 153 | }); 154 | console.log("Completed handling invoice.payment_failed", { 155 | text, 156 | path: "webhooks/stripe", 157 | method: "POST", 158 | eventType: event.type, 159 | event, 160 | }); 161 | break; 162 | default: 163 | console.error("Error onendpoint", { 164 | text, 165 | path: "webhooks/stripe", 166 | method: "POST", 167 | error: `Unhandled event: ${event.type}`, 168 | }); 169 | return NextResponse.json( 170 | { message: "Unknown event" }, 171 | { status: 400 } 172 | ); 173 | } 174 | } catch (error) { 175 | console.error("Error on endpoint", { 176 | text, 177 | path: "webhooks/stripe", 178 | method: "POST", 179 | error, 180 | }); 181 | return NextResponse.json( 182 | { message: "Webhook handler failed", error }, 183 | { status: 500 } 184 | ); 185 | } 186 | } 187 | // Return a response to acknowledge receipt of the event. 188 | return NextResponse.json({ message: "Received" }, { status: 200 }); 189 | }; 190 | -------------------------------------------------------------------------------- /src/app/components/common/Button.tsx: -------------------------------------------------------------------------------- 1 | import { cva, VariantProps } from "class-variance-authority"; 2 | import { ComponentProps } from "react"; 3 | import Link from "next/link"; 4 | 5 | const buttonStyles = cva( 6 | "flex items-center justify-center px-4 py-2 rounded-full font-medium focus:outline-none focus:ring-2 focus:ring-offset-white dark:focus:ring-offset-black focus:ring-offset-1 disabled:opacity-60 disabled:pointer-events-none hover:bg-opacity-80", 7 | { 8 | variants: { 9 | intent: { 10 | primary: "bg-indigo-500 text-white", 11 | secondary: 12 | "bg-white text-gray-900 hover:bg-gray-100 shadow-sm ring-1 ring-inset ring-gray-300", 13 | noBackground: "text-blue-950 font-medium", 14 | danger: "bg-red-500 text-white focus:ring-red-500", 15 | }, 16 | fullWidth: { 17 | true: "w-full", 18 | }, 19 | }, 20 | defaultVariants: { 21 | intent: "primary", 22 | }, 23 | } 24 | ); 25 | 26 | export interface Props 27 | extends ButtonOrLinkProps, 28 | VariantProps {} 29 | export function Button({ intent, fullWidth, ...props }: Props) { 30 | return ( 31 | 32 | ); 33 | } 34 | 35 | type ButtonOrLinkProps = ComponentProps<"button"> & ComponentProps<"a">; 36 | 37 | export interface Props extends ButtonOrLinkProps {} 38 | 39 | /** 40 | * This is a base component that will render either a button or a link, 41 | * depending on the props that are passed to it. The link rendered will 42 | * also correctly get wrapped in a next/link component to ensure ideal 43 | * page-to-page transitions. 44 | */ 45 | export function ButtonOrLink({ href, ...props }: Props) { 46 | const isLink = typeof href !== "undefined"; 47 | 48 | let content = 141 | 142 | 143 | 144 |
153 |       
162 |     
163 |   );
164 | }


--------------------------------------------------------------------------------
/src/app/components/common/FormatedJsonOutputV2.tsx:
--------------------------------------------------------------------------------
  1 | import { JSONPath } from "jsonpath-plus";
  2 | import { ChangeEvent, useEffect, useState } from "react";
  3 | import Selector from "@/app/components/common/Selector";
  4 | import dynamic from "next/dynamic";
  5 | 
  6 | const ReactJson = dynamic(() => import("react-json-view"));
  7 | 
  8 | type SpacingOption = {
  9 |   value: number;
 10 |   label: string;
 11 | };
 12 | 
 13 | const spacingOptions: SpacingOption[] = [
 14 |   { value: 2, label: "2 spaces" },
 15 |   { value: 4, label: "4 spaces" },
 16 |   { value: 8, label: "8 spaces" },
 17 |   { value: 16, label: "16 spaces" },
 18 | ];
 19 | 
 20 | type Props = {
 21 |   value: string;
 22 |   title?: string;
 23 | };
 24 | export default function FormattedJsonOutputV2({
 25 |   value,
 26 |   title = "Output",
 27 | }: Props) {
 28 |   const [jsonPathFilter, setJsonPathFilter] = useState("");
 29 |   const [numberOfSpaces, setNumberOfSpaces] = useState(2);
 30 |   const [output, setOutput] = useState("");
 31 | 
 32 |   useEffect(() => {
 33 |     setOutput(value);
 34 |   }, [value]);
 35 | 
 36 |   const handleChange = (event: ChangeEvent) => {
 37 |     const path = event.target.value;
 38 |     setJsonPathFilter(path);
 39 |     if (!path.length) {
 40 |       setOutput(value);
 41 |       return;
 42 |     }
 43 |     try {
 44 |       const json = JSON.parse(value);
 45 |       const newOutput = JSONPath({ path, json });
 46 |       setOutput(JSON.stringify(newOutput));
 47 |     } catch (error) {
 48 |       setOutput(value);
 49 |       return;
 50 |     }
 51 |   };
 52 | 
 53 |   const outputBlock = (__html: string = "") => {
 54 |     return (
 55 |       
 60 |     );
 61 |   };
 62 | 
 63 |   const JsonObject = ({ value }: { value: string }) => {
 64 |     try {
 65 |       const emptyBraces = `{}`;
 66 |       if (!value.length || typeof document === undefined) {
 67 |         return outputBlock(emptyBraces);
 68 |       }
 69 |       const jsonValue = JSON.parse(value);
 70 |       if (typeof jsonValue !== "object") return outputBlock(emptyBraces);
 71 |       setOutput(value);
 72 |       return (
 73 |         
77 | 84 |
85 | ); 86 | } catch (err) { 87 | const message = err instanceof Error ? err.message : ""; 88 | const dangerousHtml = `${message}`; 89 | return outputBlock(dangerousHtml); 90 | } 91 | }; 92 | 93 | return ( 94 |
95 |
96 |

{title}:

97 |
98 | { 102 | setNumberOfSpaces(spacingOption.value as number); 103 | }} 104 | /> 105 | 119 |
120 |
121 | 122 | 123 | 132 |
133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /src/app/components/common/FormattedMarkdownOutput.tsx: -------------------------------------------------------------------------------- 1 | import { JSONPath } from "jsonpath-plus"; 2 | import { ChangeEvent, useCallback, useEffect, useState } from "react"; 3 | import useDebounce from "@/app/hooks/useDebounce"; 4 | import Selector from "@/app/components/common/Selector"; 5 | import { marked } from "marked"; 6 | import * as DOMPurify from 'dompurify'; 7 | import 'github-markdown-css'; 8 | 9 | type Props = { 10 | input: string; 11 | title?: string; 12 | }; 13 | export default function FormattedMarkdownOutput({ 14 | input, 15 | title = "Output", 16 | }: Props) { 17 | const [output, setOutput] = useState(""); 18 | 19 | const formatMarkdown = useCallback( 20 | (input: string) => { 21 | try { 22 | return DOMPurify.sanitize( 23 | marked( 24 | // remove the most common zerowidth characters from the start of the file 25 | input.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/,"") 26 | ) 27 | ) 28 | } catch (e: any) { 29 | if (e instanceof SyntaxError) { 30 | return e.message; 31 | } 32 | } 33 | return ""; 34 | }, [] 35 | ) 36 | 37 | useEffect(() =>{ 38 | const md = formatMarkdown(input); 39 | setOutput(md); 40 | }) 41 | 42 | return ( 43 |
44 |
45 |

{title}:

46 |
47 | 56 | 65 |
66 |
67 |
76 |     
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/app/components/common/ReadOnlyTextArea.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | value: string; 3 | // eslint-disable-next-line react/require-default-props 4 | title?: string; 5 | }; 6 | export default function ReadOnlyTextArea({ value, title = "Output:" }: Props) { 7 | return ( 8 |
9 |
10 |

{title}

11 | 20 |
21 |