├── .env.example ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── app ├── (auth) │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ └── register │ │ └── page.tsx ├── (dashboard) │ ├── dashboard │ │ ├── billing │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ └── layout.tsx ├── (editor) │ └── edit │ │ ├── [id] │ │ └── page.tsx │ │ └── layout.tsx ├── (home) │ ├── layout.tsx │ └── page.tsx ├── api │ ├── api-key │ │ └── route.ts │ ├── chat │ │ └── route.ts │ ├── posts │ │ ├── [markdownId] │ │ │ └── route.ts │ │ └── route.ts │ ├── shorten-url │ │ └── route.ts │ ├── unsplash │ │ └── route.ts │ ├── user │ │ └── stripe │ │ │ └── route.ts │ └── webhooks │ │ └── stripe │ │ └── route.ts ├── error.tsx ├── layout.tsx ├── not-found.tsx └── opengraph-image.png ├── assets └── fonts │ ├── CalSans-SemiBold.ttf │ ├── CalSans-SemiBold.woff │ ├── CalSans-SemiBold.woff2 │ ├── Inter-Bold.ttf │ └── Inter-Regular.ttf ├── atoms └── editor.ts ├── components.json ├── components ├── HOCs │ └── with-desktop-only.tsx ├── bring-api-key.tsx ├── card-skeleton.tsx ├── chat-scroll-anchor.tsx ├── code-block.tsx ├── copy-button.tsx ├── dashboard │ ├── billing-form.tsx │ ├── dashboard-header.tsx │ ├── dashboard-inline-header.tsx │ ├── empty-placeholder.tsx │ ├── post-create-button.tsx │ ├── post-item.tsx │ ├── post-operations.tsx │ ├── shell.tsx │ └── user-avatar.tsx ├── editor │ ├── ai-tools-section.tsx │ ├── ai-tools.tsx │ ├── ask-ai.tsx │ ├── assets.tsx │ ├── editor-header.tsx │ ├── editor-kbd-shortcuts.tsx │ ├── editor-nav.tsx │ ├── editor-section.tsx │ ├── editor-sections-manage-panel.tsx │ ├── parse-markdown.tsx │ ├── preview-section.tsx │ ├── save-button.tsx │ ├── section-loading.tsx │ └── unsplash-dialog.tsx ├── email-templates │ ├── register.tsx │ └── signin.tsx ├── icons.tsx ├── image-with-skeleton.tsx ├── mode-toggle.tsx ├── open-source.tsx ├── providers.tsx ├── site-assets.tsx ├── site-features.tsx ├── site-footer.tsx ├── site-header.tsx ├── site-hero.tsx ├── theme-provider.tsx ├── ui │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── autotextarea.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── resizable.tsx │ ├── select.tsx │ ├── skeleton.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── tooltip.tsx │ └── use-toast.ts ├── upgrade-to-pro-dialog.tsx └── user-auth-form.tsx ├── config ├── ai.ts ├── editor.ts ├── site.ts └── subscriptions.ts ├── constants └── links.ts ├── env.mjs ├── lib ├── apiClient.ts ├── auth.ts ├── crypto.ts ├── db.ts ├── exceptions.ts ├── fonts.ts ├── hooks │ ├── use-at-bottom.tsx │ └── use-localstorage.ts ├── kv.ts ├── openai.ts ├── resend.ts ├── session.ts ├── stripe.ts ├── subscription.ts ├── utils.ts └── validations │ ├── auth.ts │ └── post.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── pages └── api │ └── auth │ └── [...nextauth].ts ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── apple-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── logos │ ├── markdx-black.svg │ └── markdx-white.svg ├── markdx-editor-frame-dark.png ├── markdx-editor-frame-light.png ├── markdx-white.png └── og.png ├── server └── utils.ts ├── styles ├── components.css ├── editor.css ├── globals.css └── mdx.css ├── tailwind.config.js ├── tsconfig.json ├── types ├── index.d.ts └── next-auth.d.ts └── utils ├── assets-list.ts ├── editor.ts ├── http.utils.ts ├── openai.ts └── world-languages.ts /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=http://localhost:3000 2 | NEXTAUTH_URL=http://localhost:3000 3 | USER_SESSION_KEY=markdx-user-session 4 | 5 | DATABASE_URL="mysql://root:root@localhost:3306/markdx?schema=public" 6 | 7 | OPENAI_API_KEY= 8 | OPENAI_MODEL=gpt-4-1106-preview 9 | 10 | # openssl rand -base64 32 : Run this in your command line and paste here the generated secret 11 | NEXTAUTH_SECRET= 12 | CRYPTO_SECRET_KEY= 13 | 14 | # Required 👆🏻 15 | # Optional 👇🏻 16 | 17 | # For get the list of languages - https://rapidapi.com 18 | RAPID_API_KEY= 19 | 20 | # For the subscription 21 | STRIPE_API_KEY= 22 | STRIPE_WEBHOOK_SECRET= 23 | STRIPE_PRO_MONTHLY_PLAN_ID= 24 | 25 | # Unsplash image search feature - https://unsplash.com 26 | UNSPLASH_ACCESS_KEY= 27 | 28 | # Upload files feature - https://cloudinary.com 29 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME= 30 | NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET= 31 | 32 | # For email authentication - https://resend.com 33 | RESEND_API_KEY= 34 | SMTP_FROM= 35 | 36 | NEXT_PUBLIC_BASE_URL=http://localhost:3000 37 | 38 | GOOGLE_CLIENT_ID= 39 | GOOGLE_CLIENT_SECRET= 40 | 41 | # Shorten URL - https://urlbae.com 42 | URLBAE_API_KEY= 43 | 44 | # For markdown caching - https;//vercel.com/kv 45 | KV_URL="redis://******:******@natural-orca-37096.upstash.io:37096" 46 | KV_REST_API_URL="https://natural-orca-37096.upstash.io" 47 | KV_REST_API_TOKEN="********" 48 | KV_REST_API_READ_ONLY_TOKEN="********" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "plugin:tailwindcss/recommended", 7 | "prettier" 8 | ], 9 | "plugins": ["tailwindcss", "@typescript-eslint"], 10 | "rules": { 11 | "@next/next/no-html-link-for-pages": "off", 12 | "react/jsx-key": "off", 13 | "tailwindcss/no-custom-classname": "off", 14 | "tailwindcss/classnames-order": "error", 15 | "prefer-const": "off", 16 | "@typescript-eslint/no-unused-vars": "error", 17 | "react-hooks/rules-of-hooks": "off" 18 | }, 19 | "settings": { 20 | "tailwindcss": { 21 | "callees": ["cn"], 22 | "config": "tailwind.config.js" 23 | }, 24 | "next": { 25 | "rootDir": true 26 | } 27 | }, 28 | "overrides": [ 29 | { 30 | "files": ["*.ts", "*.tsx"], 31 | "parser": "@typescript-eslint/parser" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.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 | # vs code 35 | .vscode 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | pnpm-lock.yaml 42 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn tsc --noEmit && yarn eslint . && yarn prettier --write . -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "importOrder": [ 8 | "^(react/(.*)$)|^(react$)", 9 | "^(next/(.*)$)|^(next$)", 10 | "", 11 | "", 12 | "^types$", 13 | "^@/env(.*)$", 14 | "^@/types/(.*)$", 15 | "^@/config/(.*)$", 16 | "^@/lib/(.*)$", 17 | "^@/hooks/(.*)$", 18 | "^@/components/ui/(.*)$", 19 | "^@/components/(.*)$", 20 | "^@/styles/(.*)$", 21 | "^@/app/(.*)$", 22 | "", 23 | "^[./]" 24 | ], 25 | "importOrderSeparation": false, 26 | "importOrderSortSpecifiers": true, 27 | "importOrderBuiltinModulesToTop": true, 28 | "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], 29 | "importOrderMergeDuplicateImports": true, 30 | "importOrderCombineTypeAndValueImports": true, 31 | "plugins": [ 32 | "@ianvs/prettier-plugin-sort-imports", 33 | "prettier-plugin-tailwindcss" 34 | ], 35 | "pluginSearchDirs": false, 36 | "tailwindConfig": "./tailwind.config.js" 37 | } 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to our project! Here's how you can help: 4 | 5 | ## Reporting Issues 6 | 7 | - Use GitHub Issues to report bugs. 8 | - Provide as much detail as you can. 9 | 10 | ## Requests for new features 11 | 12 | If you have a request for a new feature, please open a discussion on GitHub. We'll be happy to help you out. 13 | 14 | ## Making Changes 15 | 16 | - Fork the repository and create a branch for your work. 17 | - Make your changes. 18 | - Submit a pull request with a clear description of what you've done. 19 | - 20 | Before you commit, run `npm run lint` to check your code for errors and `npm run format:write` to format your code. 21 | 22 | ## Guidelines 23 | 24 | - Keep code changes simple and focused. 25 | - Follow the existing coding style. 26 | 27 | ## Commit Convention 28 | 29 | Before you create a Pull Request, please check whether your commits comply with 30 | the commit conventions used in this repository. 31 | 32 | When you create a commit we kindly ask you to follow the convention 33 | `category(scope or module): message` in your commit message while using one of 34 | the following categories: 35 | 36 | - `feat / feature`: all changes that introduce completely new code or new 37 | features 38 | - `fix`: changes that fix a bug (ideally you will additionally reference an 39 | issue if present) 40 | - `refactor`: any code related change that is not a fix nor a feature 41 | - `docs`: changing existing or creating new documentation (i.e. README, docs for 42 | usage of a lib or cli usage) 43 | - `build`: all changes regarding the build of the software, changes to 44 | dependencies or the addition of new dependencies 45 | - `test`: all changes regarding tests (adding new tests or changing existing 46 | ones) 47 | - `ci`: all changes regarding the configuration of continuous integration (i.e. 48 | github actions, ci system) 49 | - `chore`: all changes to the repository that do not fit into any of the above 50 | categories 51 | 52 | e.g. `feat(components): add new prop to the avatar component` 53 | 54 | If you are interested in the detailed specification you can visit 55 | https://www.conventionalcommits.org/ or check out the 56 | [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines). 57 | 58 | ## Remember 59 | 60 | - All contributions are under the same license as the project. 61 | 62 | Your contributions are greatly appreciated! 63 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Arshad Yaseen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MarkDX 2 | 3 | Meet MarkDX, an AI-powered markdown editor for top-notch markdown writing. 4 | 5 | ![MarkDX Screenshot](/public/og.png) 6 | 7 | ## Running Locally 8 | 9 | 1. Install dependencies using npm: 10 | 11 | ```sh 12 | npm install 13 | ``` 14 | 15 | 2. Copy `.env.example` to `.env.local` and update the variables. 16 | 17 | ```sh 18 | cp .env.example .env.local 19 | ``` 20 | 21 | 3. Start the development server: 22 | 23 | ```sh 24 | npm run dev 25 | ``` 26 | 27 | Read more about how to contribute to this project in [contributing guide](/CONTRIBUTING.md). 28 | 29 | ## License 30 | 31 | Licensed under the [MIT license](https://github.com/arshad-yaseen/markdx/blob/main/LICENSE.md). 32 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | interface AuthLayoutProps { 2 | children: React.ReactNode 3 | } 4 | 5 | export default function AuthLayout({ children }: AuthLayoutProps) { 6 | return
{children}
7 | } 8 | -------------------------------------------------------------------------------- /app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import Link from "next/link" 3 | import { ChevronLeftIcon } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | import { buttonVariants } from "@/components/ui/button" 7 | import SiteAssets from "@/components/site-assets" 8 | import { UserAuthForm } from "@/components/user-auth-form" 9 | 10 | export const metadata = { 11 | title: "Login", 12 | description: "Login to your account", 13 | } 14 | 15 | function LoginPage() { 16 | return ( 17 |
18 | 25 | <> 26 | 27 | Back 28 | 29 | 30 |
31 |
32 | 33 |

34 | Welcome back 35 |

36 |

37 | Enter your email to sign in to your account 38 |

39 |
40 | 41 |

42 | 46 | Don't have an account? Sign Up 47 | 48 |

49 |
50 |
51 | ) 52 | } 53 | 54 | export default LoginPage 55 | -------------------------------------------------------------------------------- /app/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { cn } from "@/lib/utils" 4 | import { buttonVariants } from "@/components/ui/button" 5 | import SiteAssets from "@/components/site-assets" 6 | import { UserAuthForm } from "@/components/user-auth-form" 7 | 8 | export const metadata = { 9 | title: "Create an account", 10 | description: "Create an account to get started.", 11 | } 12 | 13 | export default function RegisterPage() { 14 | return ( 15 |
16 | 23 | Login 24 | 25 |
26 |
27 |
28 |
29 | 30 |

31 | Create an account 32 |

33 |

34 | Enter your email below to create your account 35 |

36 |
37 | 38 |

39 | By clicking continue, you agree to our{" "} 40 | 44 | Terms of Service 45 | {" "} 46 | and{" "} 47 | 51 | Privacy Policy 52 | 53 | . 54 |

55 |
56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /app/(dashboard)/dashboard/billing/loading.tsx: -------------------------------------------------------------------------------- 1 | import { CardSkeleton } from "@/components/card-skeleton" 2 | import { DashboardInlineHeader } from "@/components/dashboard/dashboard-inline-header" 3 | import { DashboardShell } from "@/components/dashboard/shell" 4 | 5 | export default function DashboardBillingLoading() { 6 | return ( 7 | 8 | 12 |
13 | 14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /app/(dashboard)/dashboard/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { authOptions } from "@/lib/auth" 4 | import { getCurrentUser } from "@/lib/session" 5 | import { stripe } from "@/lib/stripe" 6 | import { getUserSubscriptionPlan } from "@/lib/subscription" 7 | import { BillingForm } from "@/components/dashboard/billing-form" 8 | import { DashboardInlineHeader } from "@/components/dashboard/dashboard-inline-header" 9 | import { DashboardShell } from "@/components/dashboard/shell" 10 | 11 | export const metadata = { 12 | title: "Billing", 13 | description: "Manage billing and your subscription plan.", 14 | } 15 | 16 | export default async function BillingPage() { 17 | const { sessionUser: user } = await getCurrentUser() 18 | 19 | if (!user) { 20 | redirect(authOptions?.pages?.signIn || "/login") 21 | } 22 | 23 | const subscriptionPlan = await getUserSubscriptionPlan(user.id) 24 | 25 | // If user has a pro plan, check cancel status on Stripe. 26 | let isCanceled = false 27 | let isPro = subscriptionPlan.isPro && subscriptionPlan.stripeSubscriptionId 28 | if (subscriptionPlan.isPro && subscriptionPlan.stripeSubscriptionId) { 29 | const stripePlan = await stripe.subscriptions.retrieve( 30 | subscriptionPlan.stripeSubscriptionId 31 | ) 32 | isCanceled = stripePlan.cancel_at_period_end 33 | } 34 | 35 | return ( 36 | 37 | 41 |
42 | 48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /app/(dashboard)/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { DashboardInlineHeader } from "@/components/dashboard/dashboard-inline-header" 4 | import { PostCreateButton } from "@/components/dashboard/post-create-button" 5 | import { PostItem } from "@/components/dashboard/post-item" 6 | import { DashboardShell } from "@/components/dashboard/shell" 7 | 8 | export default function DashboardLoading() { 9 | return ( 10 | 11 | 15 | 16 | 17 |
18 | 19 | 20 | 21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/(dashboard)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | import { getMarkdownTitle } from "@/utils/editor" 3 | 4 | import { authOptions } from "@/lib/auth" 5 | import { db } from "@/lib/db" 6 | import { getCurrentUser } from "@/lib/session" 7 | import { DashboardInlineHeader } from "@/components/dashboard/dashboard-inline-header" 8 | import { EmptyPlaceholder } from "@/components/dashboard/empty-placeholder" 9 | import { PostCreateButton } from "@/components/dashboard/post-create-button" 10 | import { PostItem } from "@/components/dashboard/post-item" 11 | import { DashboardShell } from "@/components/dashboard/shell" 12 | import { Icons } from "@/components/icons" 13 | 14 | async function Dashboard() { 15 | const { sessionUser: user } = await getCurrentUser() 16 | 17 | if (!user) { 18 | redirect(authOptions?.pages?.signIn || "/login") 19 | } 20 | 21 | const posts = await db.markdownPost.findMany({ 22 | where: { 23 | userId: user.id, 24 | }, 25 | select: { 26 | id: true, 27 | markdownId: true, 28 | userId: true, 29 | createdAt: true, 30 | postCodes: true, 31 | }, 32 | orderBy: { 33 | updatedAt: "desc", 34 | }, 35 | }) 36 | 37 | return ( 38 | <> 39 | 40 | 44 | 45 | 46 |
47 | {posts?.length ? ( 48 |
49 | {posts.map((post) => ( 50 | 58 | ))} 59 |
60 | ) : ( 61 | 62 | 63 | 64 | No markdowns created 65 | 66 | 67 | 68 | 69 | )} 70 |
71 | 72 | {/* { 73 | posts.length > 0 && ( 74 | <> 75 |

78 | Feedback 79 |

80 |
81 |