├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── auth.config.ts ├── codeExecutions.ts ├── http.ts ├── lemonSqueezy.ts ├── schema.ts ├── snippets.ts ├── tsconfig.json └── users.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── bash.png ├── cpp.png ├── csharp.png ├── go.png ├── java.png ├── javascript.png ├── js.png ├── python.png ├── ruby.png ├── rust.png ├── screenshot-for-readme.png ├── swift.png ├── ts.png ├── typescript.png └── vercel.svg ├── src ├── app │ ├── (root) │ │ ├── _components │ │ │ ├── EditorPanel.tsx │ │ │ ├── EditorPanelSkeleton.tsx │ │ │ ├── Header.tsx │ │ │ ├── HeaderProfileBtn.tsx │ │ │ ├── LanguageSelector.tsx │ │ │ ├── OutputPanel.tsx │ │ │ ├── RunButton.tsx │ │ │ ├── RunningCodeSkeleton.tsx │ │ │ ├── ShareSnippetDialog.tsx │ │ │ └── ThemeSelector.tsx │ │ ├── _constants │ │ │ └── index.ts │ │ └── page.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ ├── layout.tsx │ ├── pricing │ │ ├── _components │ │ │ ├── FeatureCategory.tsx │ │ │ ├── FeatureItem.tsx │ │ │ ├── ProPlanView.tsx │ │ │ └── UpgradeButton.tsx │ │ ├── _constants │ │ │ └── index.ts │ │ └── page.tsx │ ├── profile │ │ ├── _components │ │ │ ├── CodeBlock.tsx │ │ │ ├── ProfileHeader.tsx │ │ │ └── ProfileHeaderSkeleton.tsx │ │ └── page.tsx │ └── snippets │ │ ├── [id] │ │ ├── _components │ │ │ ├── CodeBlock.tsx │ │ │ ├── Comment.tsx │ │ │ ├── CommentContent.tsx │ │ │ ├── CommentForm.tsx │ │ │ ├── Comments.tsx │ │ │ ├── CopyButton.tsx │ │ │ └── SnippetLoadingSkeleton.tsx │ │ └── page.tsx │ │ ├── _components │ │ ├── SnippetCard.tsx │ │ └── SnippetsPageSkeleton.tsx │ │ └── page.tsx ├── components │ ├── Footer.tsx │ ├── LoginButton.tsx │ ├── NavigationHeader.tsx │ ├── StarButton.tsx │ └── providers │ │ └── ConvexClientProvider.tsx ├── hooks │ └── useMounted.tsx ├── middleware.ts ├── store │ └── useCodeEditorStore.ts └── types │ └── index.ts ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@next/next/no-img-element": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Burak 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 |

✨ SaaS Code Editor - Next.js 15 ✨

2 | 3 | ![Demo App](/public/screenshot-for-readme.png) 4 | 5 | [Watch Full Tutorial on Youtube](https://youtu.be/fGkRQgf6Scw) 6 | 7 | Highlights: 8 | 9 | - 🚀 Tech stack: Next.js 15 + Convex + Clerk + TypeScript 10 | - 💻 Online IDE with multi-language support (10 languages) 11 | - 🎨 Customizable experience with 5 VSCode themes 12 | - ✨ Smart output handling with Success & Error states 13 | - 💎 Flexible pricing with Free & Pro plans 14 | - 🤝 Community-driven code sharing system 15 | - 🔍 Advanced filtering & search capabilities 16 | - 👤 Personal profile with execution history tracking 17 | - 📊 Comprehensive statistics dashboard 18 | - ⚙️ Customizable font size controls 19 | - 🔗 Webhook integration support 20 | - 🌟 Professional deployment walkthrough 21 | 22 | ### Setup .env file 23 | 24 | ```js 25 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 26 | CLERK_SECRET_KEY= 27 | CONVEX_DEPLOYMENT= 28 | NEXT_PUBLIC_CONVEX_URL= 29 | ``` 30 | 31 | ### Add these env to Convex Dashboard 32 | 33 | ```js 34 | CLERK_WEBHOOK_SECRET= 35 | LEMON_SQUEEZY_WEBHOOK_SECRET= 36 | ``` 37 | 38 | ### Run the app 39 | 40 | ```shell 41 | npm run dev 42 | ``` 43 | -------------------------------------------------------------------------------- /convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. 4 | See https://docs.convex.dev/functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | handler: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | handler: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result), 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | ApiFromModules, 13 | FilterApi, 14 | FunctionReference, 15 | } from "convex/server"; 16 | import type * as codeExecutions from "../codeExecutions.js"; 17 | import type * as http from "../http.js"; 18 | import type * as lemonSqueezy from "../lemonSqueezy.js"; 19 | import type * as snippets from "../snippets.js"; 20 | import type * as users from "../users.js"; 21 | 22 | /** 23 | * A utility for referencing Convex functions in your app's API. 24 | * 25 | * Usage: 26 | * ```js 27 | * const myFunctionReference = api.myModule.myFunction; 28 | * ``` 29 | */ 30 | declare const fullApi: ApiFromModules<{ 31 | codeExecutions: typeof codeExecutions; 32 | http: typeof http; 33 | lemonSqueezy: typeof lemonSqueezy; 34 | snippets: typeof snippets; 35 | users: typeof users; 36 | }>; 37 | export declare const api: FilterApi< 38 | typeof fullApi, 39 | FunctionReference 40 | >; 41 | export declare const internal: FilterApi< 42 | typeof fullApi, 43 | FunctionReference 44 | >; 45 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { anyApi } from "convex/server"; 12 | 13 | /** 14 | * A utility for referencing Convex functions in your app's API. 15 | * 16 | * Usage: 17 | * ```js 18 | * const myFunctionReference = api.myModule.myFunction; 19 | * ``` 20 | */ 21 | export const api = anyApi; 22 | export const internal = anyApi; 23 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | DataModelFromSchemaDefinition, 13 | DocumentByName, 14 | TableNamesInDataModel, 15 | SystemTableNames, 16 | } from "convex/server"; 17 | import type { GenericId } from "convex/values"; 18 | import schema from "../schema.js"; 19 | 20 | /** 21 | * The names of all of your Convex tables. 22 | */ 23 | export type TableNames = TableNamesInDataModel; 24 | 25 | /** 26 | * The type of a document stored in Convex. 27 | * 28 | * @typeParam TableName - A string literal type of the table name (like "users"). 29 | */ 30 | export type Doc = DocumentByName< 31 | DataModel, 32 | TableName 33 | >; 34 | 35 | /** 36 | * An identifier for a document in Convex. 37 | * 38 | * Convex documents are uniquely identified by their `Id`, which is accessible 39 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 40 | * 41 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 42 | * 43 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 44 | * strings when type checking. 45 | * 46 | * @typeParam TableName - A string literal type of the table name (like "users"). 47 | */ 48 | export type Id = 49 | GenericId; 50 | 51 | /** 52 | * A type describing your Convex data model. 53 | * 54 | * This type includes information about what tables you have, the type of 55 | * documents stored in those tables, and the indexes defined on them. 56 | * 57 | * This type is used to parameterize methods like `queryGeneric` and 58 | * `mutationGeneric` to make them type-safe. 59 | */ 60 | export type DataModel = DataModelFromSchemaDefinition; 61 | -------------------------------------------------------------------------------- /convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { 12 | ActionBuilder, 13 | HttpActionBuilder, 14 | MutationBuilder, 15 | QueryBuilder, 16 | GenericActionCtx, 17 | GenericMutationCtx, 18 | GenericQueryCtx, 19 | GenericDatabaseReader, 20 | GenericDatabaseWriter, 21 | } from "convex/server"; 22 | import type { DataModel } from "./dataModel.js"; 23 | 24 | /** 25 | * Define a query in this Convex app's public API. 26 | * 27 | * This function will be allowed to read your Convex database and will be accessible from the client. 28 | * 29 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 30 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 31 | */ 32 | export declare const query: QueryBuilder; 33 | 34 | /** 35 | * Define a query that is only accessible from other Convex functions (but not from the client). 36 | * 37 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 38 | * 39 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 40 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 41 | */ 42 | export declare const internalQuery: QueryBuilder; 43 | 44 | /** 45 | * Define a mutation in this Convex app's public API. 46 | * 47 | * This function will be allowed to modify your Convex database and will be accessible from the client. 48 | * 49 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 50 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 51 | */ 52 | export declare const mutation: MutationBuilder; 53 | 54 | /** 55 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 56 | * 57 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 58 | * 59 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 60 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 61 | */ 62 | export declare const internalMutation: MutationBuilder; 63 | 64 | /** 65 | * Define an action in this Convex app's public API. 66 | * 67 | * An action is a function which can execute any JavaScript code, including non-deterministic 68 | * code and code with side-effects, like calling third-party services. 69 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 70 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 71 | * 72 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 73 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 74 | */ 75 | export declare const action: ActionBuilder; 76 | 77 | /** 78 | * Define an action that is only accessible from other Convex functions (but not from the client). 79 | * 80 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 81 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 82 | */ 83 | export declare const internalAction: ActionBuilder; 84 | 85 | /** 86 | * Define an HTTP action. 87 | * 88 | * This function will be used to respond to HTTP requests received by a Convex 89 | * deployment if the requests matches the path and method where this action 90 | * is routed. Be sure to route your action in `convex/http.js`. 91 | * 92 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 93 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 94 | */ 95 | export declare const httpAction: HttpActionBuilder; 96 | 97 | /** 98 | * A set of services for use within Convex query functions. 99 | * 100 | * The query context is passed as the first argument to any Convex query 101 | * function run on the server. 102 | * 103 | * This differs from the {@link MutationCtx} because all of the services are 104 | * read-only. 105 | */ 106 | export type QueryCtx = GenericQueryCtx; 107 | 108 | /** 109 | * A set of services for use within Convex mutation functions. 110 | * 111 | * The mutation context is passed as the first argument to any Convex mutation 112 | * function run on the server. 113 | */ 114 | export type MutationCtx = GenericMutationCtx; 115 | 116 | /** 117 | * A set of services for use within Convex action functions. 118 | * 119 | * The action context is passed as the first argument to any Convex action 120 | * function run on the server. 121 | */ 122 | export type ActionCtx = GenericActionCtx; 123 | 124 | /** 125 | * An interface to read from the database within Convex query functions. 126 | * 127 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 128 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 129 | * building a query. 130 | */ 131 | export type DatabaseReader = GenericDatabaseReader; 132 | 133 | /** 134 | * An interface to read from and write to the database within Convex mutation 135 | * functions. 136 | * 137 | * Convex guarantees that all writes within a single mutation are 138 | * executed atomically, so you never have to worry about partial writes leaving 139 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 140 | * for the guarantees Convex provides your functions. 141 | */ 142 | export type DatabaseWriter = GenericDatabaseWriter; 143 | -------------------------------------------------------------------------------- /convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { 12 | actionGeneric, 13 | httpActionGeneric, 14 | queryGeneric, 15 | mutationGeneric, 16 | internalActionGeneric, 17 | internalMutationGeneric, 18 | internalQueryGeneric, 19 | } from "convex/server"; 20 | 21 | /** 22 | * Define a query in this Convex app's public API. 23 | * 24 | * This function will be allowed to read your Convex database and will be accessible from the client. 25 | * 26 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 27 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 28 | */ 29 | export const query = queryGeneric; 30 | 31 | /** 32 | * Define a query that is only accessible from other Convex functions (but not from the client). 33 | * 34 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 35 | * 36 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 37 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 38 | */ 39 | export const internalQuery = internalQueryGeneric; 40 | 41 | /** 42 | * Define a mutation in this Convex app's public API. 43 | * 44 | * This function will be allowed to modify your Convex database and will be accessible from the client. 45 | * 46 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 47 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 48 | */ 49 | export const mutation = mutationGeneric; 50 | 51 | /** 52 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 53 | * 54 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 55 | * 56 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 57 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 58 | */ 59 | export const internalMutation = internalMutationGeneric; 60 | 61 | /** 62 | * Define an action in this Convex app's public API. 63 | * 64 | * An action is a function which can execute any JavaScript code, including non-deterministic 65 | * code and code with side-effects, like calling third-party services. 66 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 67 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 68 | * 69 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 70 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 71 | */ 72 | export const action = actionGeneric; 73 | 74 | /** 75 | * Define an action that is only accessible from other Convex functions (but not from the client). 76 | * 77 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 78 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 79 | */ 80 | export const internalAction = internalActionGeneric; 81 | 82 | /** 83 | * Define a Convex HTTP action. 84 | * 85 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 86 | * as its second. 87 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 88 | */ 89 | export const httpAction = httpActionGeneric; 90 | -------------------------------------------------------------------------------- /convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | domain: "https://enabling-alien-47.clerk.accounts.dev/", 5 | applicationID: "convex", 6 | }, 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /convex/codeExecutions.ts: -------------------------------------------------------------------------------- 1 | import { ConvexError, v } from "convex/values"; 2 | import { mutation, query } from "./_generated/server"; 3 | import { paginationOptsValidator } from "convex/server"; 4 | 5 | export const saveExecution = mutation({ 6 | args: { 7 | language: v.string(), 8 | code: v.string(), 9 | // we could have either one of them, or both at the same time 10 | output: v.optional(v.string()), 11 | error: v.optional(v.string()), 12 | }, 13 | handler: async (ctx, args) => { 14 | const identity = await ctx.auth.getUserIdentity(); 15 | if (!identity) throw new ConvexError("Not authenticated"); 16 | 17 | // check pro status 18 | const user = await ctx.db 19 | .query("users") 20 | .withIndex("by_user_id") 21 | .filter((q) => q.eq(q.field("userId"), identity.subject)) 22 | .first(); 23 | 24 | if (!user?.isPro && args.language !== "javascript") { 25 | throw new ConvexError("Pro subscription required to use this language"); 26 | } 27 | 28 | await ctx.db.insert("codeExecutions", { 29 | ...args, 30 | userId: identity.subject, 31 | }); 32 | }, 33 | }); 34 | 35 | export const getUserExecutions = query({ 36 | args: { 37 | userId: v.string(), 38 | paginationOpts: paginationOptsValidator, 39 | }, 40 | handler: async (ctx, args) => { 41 | return await ctx.db 42 | .query("codeExecutions") 43 | .withIndex("by_user_id") 44 | .filter((q) => q.eq(q.field("userId"), args.userId)) 45 | .order("desc") 46 | .paginate(args.paginationOpts); 47 | }, 48 | }); 49 | 50 | export const getUserStats = query({ 51 | args: { userId: v.string() }, 52 | handler: async (ctx, args) => { 53 | const executions = await ctx.db 54 | .query("codeExecutions") 55 | .withIndex("by_user_id") 56 | .filter((q) => q.eq(q.field("userId"), args.userId)) 57 | .collect(); 58 | 59 | // Get starred snippets 60 | const starredSnippets = await ctx.db 61 | .query("stars") 62 | .withIndex("by_user_id") 63 | .filter((q) => q.eq(q.field("userId"), args.userId)) 64 | .collect(); 65 | 66 | // Get all starred snippet details to analyze languages 67 | const snippetIds = starredSnippets.map((star) => star.snippetId); 68 | const snippetDetails = await Promise.all(snippetIds.map((id) => ctx.db.get(id))); 69 | 70 | // Calculate most starred language 71 | const starredLanguages = snippetDetails.filter(Boolean).reduce( 72 | (acc, curr) => { 73 | if (curr?.language) { 74 | acc[curr.language] = (acc[curr.language] || 0) + 1; 75 | } 76 | return acc; 77 | }, 78 | {} as Record 79 | ); 80 | 81 | const mostStarredLanguage = 82 | Object.entries(starredLanguages).sort(([, a], [, b]) => b - a)[0]?.[0] ?? "N/A"; 83 | 84 | // Calculate execution stats 85 | const last24Hours = executions.filter( 86 | (e) => e._creationTime > Date.now() - 24 * 60 * 60 * 1000 87 | ).length; 88 | 89 | const languageStats = executions.reduce( 90 | (acc, curr) => { 91 | acc[curr.language] = (acc[curr.language] || 0) + 1; 92 | return acc; 93 | }, 94 | {} as Record 95 | ); 96 | 97 | const languages = Object.keys(languageStats); 98 | const favoriteLanguage = languages.length 99 | ? languages.reduce((a, b) => (languageStats[a] > languageStats[b] ? a : b)) 100 | : "N/A"; 101 | 102 | return { 103 | totalExecutions: executions.length, 104 | languagesCount: languages.length, 105 | languages: languages, 106 | last24Hours, 107 | favoriteLanguage, 108 | languageStats, 109 | mostStarredLanguage, 110 | }; 111 | }, 112 | }); 113 | -------------------------------------------------------------------------------- /convex/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRouter } from "convex/server"; 2 | import { httpAction } from "./_generated/server"; 3 | import { Webhook } from "svix"; 4 | import { WebhookEvent } from "@clerk/nextjs/server"; 5 | import { api, internal } from "./_generated/api"; 6 | 7 | const http = httpRouter(); 8 | 9 | http.route({ 10 | path: "/lemon-squeezy-webhook", 11 | method: "POST", 12 | handler: httpAction(async (ctx, request) => { 13 | const payloadString = await request.text(); 14 | const signature = request.headers.get("X-Signature"); 15 | 16 | if (!signature) { 17 | return new Response("Missing X-Signature header", { status: 400 }); 18 | } 19 | 20 | try { 21 | const payload = await ctx.runAction(internal.lemonSqueezy.verifyWebhook, { 22 | payload: payloadString, 23 | signature, 24 | }); 25 | 26 | if (payload.meta.event_name === "order_created") { 27 | const { data } = payload; 28 | 29 | const { success } = await ctx.runMutation(api.users.upgradeToPro, { 30 | email: data.attributes.user_email, 31 | lemonSqueezyCustomerId: data.attributes.customer_id.toString(), 32 | lemonSqueezyOrderId: data.id, 33 | amount: data.attributes.total, 34 | }); 35 | 36 | if (success) { 37 | // optionally do anything here 38 | } 39 | } 40 | 41 | return new Response("Webhook processed successfully", { status: 200 }); 42 | } catch (error) { 43 | console.log("Webhook error:", error); 44 | return new Response("Error processing webhook", { status: 500 }); 45 | } 46 | }), 47 | }); 48 | 49 | http.route({ 50 | path: "/clerk-webhook", 51 | method: "POST", 52 | handler: httpAction(async (ctx, request) => { 53 | const webhookSecret = process.env.CLERK_WEBHOOK_SECRET; 54 | if (!webhookSecret) { 55 | throw new Error("Missing CLERK_WEBHOOK_SECRET environment variable"); 56 | } 57 | 58 | const svix_id = request.headers.get("svix-id"); 59 | const svix_signature = request.headers.get("svix-signature"); 60 | const svix_timestamp = request.headers.get("svix-timestamp"); 61 | 62 | if (!svix_id || !svix_signature || !svix_timestamp) { 63 | return new Response("Error occurred -- no svix headers", { 64 | status: 400, 65 | }); 66 | } 67 | 68 | const payload = await request.json(); 69 | const body = JSON.stringify(payload); 70 | 71 | const wh = new Webhook(webhookSecret); 72 | let evt: WebhookEvent; 73 | 74 | try { 75 | evt = wh.verify(body, { 76 | "svix-id": svix_id, 77 | "svix-timestamp": svix_timestamp, 78 | "svix-signature": svix_signature, 79 | }) as WebhookEvent; 80 | } catch (err) { 81 | console.error("Error verifying webhook:", err); 82 | return new Response("Error occurred", { status: 400 }); 83 | } 84 | 85 | const eventType = evt.type; 86 | if (eventType === "user.created") { 87 | // save the user to convex db 88 | const { id, email_addresses, first_name, last_name } = evt.data; 89 | 90 | const email = email_addresses[0].email_address; 91 | const name = `${first_name || ""} ${last_name || ""}`.trim(); 92 | 93 | try { 94 | await ctx.runMutation(api.users.syncUser, { 95 | userId: id, 96 | email, 97 | name, 98 | }); 99 | } catch (error) { 100 | console.log("Error creating user:", error); 101 | return new Response("Error creating user", { status: 500 }); 102 | } 103 | } 104 | 105 | return new Response("Webhook processed successfully", { status: 200 }); 106 | }), 107 | }); 108 | 109 | export default http; 110 | -------------------------------------------------------------------------------- /convex/lemonSqueezy.ts: -------------------------------------------------------------------------------- 1 | "use node"; 2 | import { v } from "convex/values"; 3 | import { internalAction } from "./_generated/server"; 4 | import { createHmac } from "crypto"; 5 | 6 | const webhookSecret = process.env.LEMON_SQUEEZY_WEBHOOK_SECRET!; 7 | 8 | function verifySignature(payload: string, signature: string): boolean { 9 | const hmac = createHmac("sha256", webhookSecret); 10 | const computedSignature = hmac.update(payload).digest("hex"); 11 | return signature === computedSignature; 12 | } 13 | 14 | export const verifyWebhook = internalAction({ 15 | args: { 16 | payload: v.string(), 17 | signature: v.string(), 18 | }, 19 | handler: async (ctx, args) => { 20 | const isValid = verifySignature(args.payload, args.signature); 21 | 22 | if (!isValid) { 23 | throw new Error("Invalid signature"); 24 | } 25 | 26 | return JSON.parse(args.payload); 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | users: defineTable({ 6 | userId: v.string(), // clerkId 7 | email: v.string(), 8 | name: v.string(), 9 | isPro: v.boolean(), 10 | proSince: v.optional(v.number()), 11 | lemonSqueezyCustomerId: v.optional(v.string()), 12 | lemonSqueezyOrderId: v.optional(v.string()), 13 | }).index("by_user_id", ["userId"]), 14 | 15 | codeExecutions: defineTable({ 16 | userId: v.string(), 17 | language: v.string(), 18 | code: v.string(), 19 | output: v.optional(v.string()), 20 | error: v.optional(v.string()), 21 | }).index("by_user_id", ["userId"]), 22 | 23 | snippets: defineTable({ 24 | userId: v.string(), 25 | title: v.string(), 26 | language: v.string(), 27 | code: v.string(), 28 | userName: v.string(), // store user's name for easy access 29 | }).index("by_user_id", ["userId"]), 30 | 31 | snippetComments: defineTable({ 32 | snippetId: v.id("snippets"), 33 | userId: v.string(), 34 | userName: v.string(), 35 | content: v.string(), // This will store HTML content 36 | }).index("by_snippet_id", ["snippetId"]), 37 | 38 | stars: defineTable({ 39 | userId: v.string(), 40 | snippetId: v.id("snippets"), 41 | }) 42 | .index("by_user_id", ["userId"]) 43 | .index("by_snippet_id", ["snippetId"]) 44 | .index("by_user_id_and_snippet_id", ["userId", "snippetId"]), 45 | }); 46 | -------------------------------------------------------------------------------- /convex/snippets.ts: -------------------------------------------------------------------------------- 1 | import { v } from "convex/values"; 2 | import { mutation, query } from "./_generated/server"; 3 | 4 | export const createSnippet = mutation({ 5 | args: { 6 | title: v.string(), 7 | language: v.string(), 8 | code: v.string(), 9 | }, 10 | handler: async (ctx, args) => { 11 | const identity = await ctx.auth.getUserIdentity(); 12 | if (!identity) throw new Error("Not authenticated"); 13 | 14 | const user = await ctx.db 15 | .query("users") 16 | .withIndex("by_user_id") 17 | .filter((q) => q.eq(q.field("userId"), identity.subject)) 18 | .first(); 19 | 20 | if (!user) throw new Error("User not found"); 21 | 22 | const snippetId = await ctx.db.insert("snippets", { 23 | userId: identity.subject, 24 | userName: user.name, 25 | title: args.title, 26 | language: args.language, 27 | code: args.code, 28 | }); 29 | 30 | return snippetId; 31 | }, 32 | }); 33 | 34 | export const deleteSnippet = mutation({ 35 | args: { 36 | snippetId: v.id("snippets"), 37 | }, 38 | 39 | handler: async (ctx, args) => { 40 | const identity = await ctx.auth.getUserIdentity(); 41 | if (!identity) throw new Error("Not authenticated"); 42 | 43 | const snippet = await ctx.db.get(args.snippetId); 44 | if (!snippet) throw new Error("Snippet not found"); 45 | 46 | if (snippet.userId !== identity.subject) { 47 | throw new Error("Not authorized to delete this snippet"); 48 | } 49 | 50 | const comments = await ctx.db 51 | .query("snippetComments") 52 | .withIndex("by_snippet_id") 53 | .filter((q) => q.eq(q.field("snippetId"), args.snippetId)) 54 | .collect(); 55 | 56 | for (const comment of comments) { 57 | await ctx.db.delete(comment._id); 58 | } 59 | 60 | const stars = await ctx.db 61 | .query("stars") 62 | .withIndex("by_snippet_id") 63 | .filter((q) => q.eq(q.field("snippetId"), args.snippetId)) 64 | .collect(); 65 | 66 | for (const star of stars) { 67 | await ctx.db.delete(star._id); 68 | } 69 | 70 | await ctx.db.delete(args.snippetId); 71 | }, 72 | }); 73 | 74 | export const starSnippet = mutation({ 75 | args: { 76 | snippetId: v.id("snippets"), 77 | }, 78 | handler: async (ctx, args) => { 79 | const identity = await ctx.auth.getUserIdentity(); 80 | if (!identity) throw new Error("Not authenticated"); 81 | 82 | const existing = await ctx.db 83 | .query("stars") 84 | .withIndex("by_user_id_and_snippet_id") 85 | .filter( 86 | (q) => 87 | q.eq(q.field("userId"), identity.subject) && q.eq(q.field("snippetId"), args.snippetId) 88 | ) 89 | .first(); 90 | 91 | if (existing) { 92 | await ctx.db.delete(existing._id); 93 | } else { 94 | await ctx.db.insert("stars", { 95 | userId: identity.subject, 96 | snippetId: args.snippetId, 97 | }); 98 | } 99 | }, 100 | }); 101 | 102 | export const addComment = mutation({ 103 | args: { 104 | snippetId: v.id("snippets"), 105 | content: v.string(), 106 | }, 107 | handler: async (ctx, args) => { 108 | const identity = await ctx.auth.getUserIdentity(); 109 | if (!identity) throw new Error("Not authenticated"); 110 | 111 | const user = await ctx.db 112 | .query("users") 113 | .withIndex("by_user_id") 114 | .filter((q) => q.eq(q.field("userId"), identity.subject)) 115 | .first(); 116 | 117 | if (!user) throw new Error("User not found"); 118 | 119 | return await ctx.db.insert("snippetComments", { 120 | snippetId: args.snippetId, 121 | userId: identity.subject, 122 | userName: user.name, 123 | content: args.content, 124 | }); 125 | }, 126 | }); 127 | 128 | export const deleteComment = mutation({ 129 | args: { commentId: v.id("snippetComments") }, 130 | handler: async (ctx, args) => { 131 | const identity = await ctx.auth.getUserIdentity(); 132 | if (!identity) throw new Error("Not authenticated"); 133 | 134 | const comment = await ctx.db.get(args.commentId); 135 | if (!comment) throw new Error("Comment not found"); 136 | 137 | // Check if the user is the comment author 138 | if (comment.userId !== identity.subject) { 139 | throw new Error("Not authorized to delete this comment"); 140 | } 141 | 142 | await ctx.db.delete(args.commentId); 143 | }, 144 | }); 145 | 146 | export const getSnippets = query({ 147 | handler: async (ctx) => { 148 | const snippets = await ctx.db.query("snippets").order("desc").collect(); 149 | return snippets; 150 | }, 151 | }); 152 | 153 | export const getSnippetById = query({ 154 | args: { snippetId: v.id("snippets") }, 155 | handler: async (ctx, args) => { 156 | const snippet = await ctx.db.get(args.snippetId); 157 | if (!snippet) throw new Error("Snippet not found"); 158 | 159 | return snippet; 160 | }, 161 | }); 162 | 163 | export const getComments = query({ 164 | args: { snippetId: v.id("snippets") }, 165 | handler: async (ctx, args) => { 166 | const comments = await ctx.db 167 | .query("snippetComments") 168 | .withIndex("by_snippet_id") 169 | .filter((q) => q.eq(q.field("snippetId"), args.snippetId)) 170 | .order("desc") 171 | .collect(); 172 | 173 | return comments; 174 | }, 175 | }); 176 | 177 | export const isSnippetStarred = query({ 178 | args: { 179 | snippetId: v.id("snippets"), 180 | }, 181 | handler: async (ctx, args) => { 182 | const identity = await ctx.auth.getUserIdentity(); 183 | if (!identity) return false; 184 | 185 | const star = await ctx.db 186 | .query("stars") 187 | .withIndex("by_user_id_and_snippet_id") 188 | .filter( 189 | (q) => 190 | q.eq(q.field("userId"), identity.subject) && q.eq(q.field("snippetId"), args.snippetId) 191 | ) 192 | .first(); 193 | 194 | return !!star; 195 | }, 196 | }); 197 | 198 | export const getSnippetStarCount = query({ 199 | args: { snippetId: v.id("snippets") }, 200 | handler: async (ctx, args) => { 201 | const stars = await ctx.db 202 | .query("stars") 203 | .withIndex("by_snippet_id") 204 | .filter((q) => q.eq(q.field("snippetId"), args.snippetId)) 205 | .collect(); 206 | 207 | return stars.length; 208 | }, 209 | }); 210 | 211 | export const getStarredSnippets = query({ 212 | handler: async (ctx) => { 213 | const identity = await ctx.auth.getUserIdentity(); 214 | if (!identity) return []; 215 | 216 | const stars = await ctx.db 217 | .query("stars") 218 | .withIndex("by_user_id") 219 | .filter((q) => q.eq(q.field("userId"), identity.subject)) 220 | .collect(); 221 | 222 | const snippets = await Promise.all(stars.map((star) => ctx.db.get(star.snippetId))); 223 | 224 | return snippets.filter((snippet) => snippet !== null); 225 | }, 226 | }); 227 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | "moduleResolution": "Bundler", 11 | "jsx": "react-jsx", 12 | "skipLibCheck": true, 13 | "allowSyntheticDefaultImports": true, 14 | 15 | /* These compiler options are required by Convex */ 16 | "target": "ESNext", 17 | "lib": ["ES2021", "dom"], 18 | "forceConsistentCasingInFileNames": true, 19 | "module": "ESNext", 20 | "isolatedModules": true, 21 | "noEmit": true 22 | }, 23 | "include": ["./**/*"], 24 | "exclude": ["./_generated"] 25 | } 26 | -------------------------------------------------------------------------------- /convex/users.ts: -------------------------------------------------------------------------------- 1 | import { v } from "convex/values"; 2 | import { mutation, query } from "./_generated/server"; 3 | 4 | export const syncUser = mutation({ 5 | args: { 6 | userId: v.string(), 7 | email: v.string(), 8 | name: v.string(), 9 | }, 10 | handler: async (ctx, args) => { 11 | const existingUser = await ctx.db 12 | .query("users") 13 | .filter((q) => q.eq(q.field("userId"), args.userId)) 14 | .first(); 15 | 16 | if (!existingUser) { 17 | await ctx.db.insert("users", { 18 | userId: args.userId, 19 | email: args.email, 20 | name: args.name, 21 | isPro: false, 22 | }); 23 | } 24 | }, 25 | }); 26 | 27 | export const getUser = query({ 28 | args: { userId: v.string() }, 29 | 30 | handler: async (ctx, args) => { 31 | if (!args.userId) return null; 32 | 33 | const user = await ctx.db 34 | .query("users") 35 | .withIndex("by_user_id") 36 | .filter((q) => q.eq(q.field("userId"), args.userId)) 37 | .first(); 38 | 39 | if (!user) return null; 40 | 41 | return user; 42 | }, 43 | }); 44 | 45 | export const upgradeToPro = mutation({ 46 | args: { 47 | email: v.string(), 48 | lemonSqueezyCustomerId: v.string(), 49 | lemonSqueezyOrderId: v.string(), 50 | amount: v.number(), 51 | }, 52 | handler: async (ctx, args) => { 53 | const user = await ctx.db 54 | .query("users") 55 | .filter((q) => q.eq(q.field("email"), args.email)) 56 | .first(); 57 | 58 | if (!user) throw new Error("User not found"); 59 | 60 | await ctx.db.patch(user._id, { 61 | isPro: true, 62 | proSince: Date.now(), 63 | lemonSqueezyCustomerId: args.lemonSqueezyCustomerId, 64 | lemonSqueezyOrderId: args.lemonSqueezyOrderId, 65 | }); 66 | 67 | return { success: true }; 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-craft", 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 | "@clerk/nextjs": "^6.6.0", 13 | "@monaco-editor/react": "^4.6.0", 14 | "convex": "^1.17.3", 15 | "framer-motion": "^11.13.1", 16 | "lucide-react": "^0.464.0", 17 | "next": "15.0.3", 18 | "react": "19.0.0-rc-66855b96-20241106", 19 | "react-dom": "19.0.0-rc-66855b96-20241106", 20 | "react-hot-toast": "^2.4.1", 21 | "react-syntax-highlighter": "^15.6.1", 22 | "svix": "^1.42.0", 23 | "zustand": "^5.0.1" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20", 27 | "@types/react": "^18", 28 | "@types/react-dom": "^18", 29 | "@types/react-syntax-highlighter": "^15.5.13", 30 | "eslint": "^8", 31 | "eslint-config-next": "15.0.3", 32 | "postcss": "^8", 33 | "tailwindcss": "^3.4.1", 34 | "typescript": "^5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/bash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/bash.png -------------------------------------------------------------------------------- /public/cpp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/cpp.png -------------------------------------------------------------------------------- /public/csharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/csharp.png -------------------------------------------------------------------------------- /public/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/go.png -------------------------------------------------------------------------------- /public/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/java.png -------------------------------------------------------------------------------- /public/javascript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/javascript.png -------------------------------------------------------------------------------- /public/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/js.png -------------------------------------------------------------------------------- /public/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/python.png -------------------------------------------------------------------------------- /public/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/ruby.png -------------------------------------------------------------------------------- /public/rust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/rust.png -------------------------------------------------------------------------------- /public/screenshot-for-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/screenshot-for-readme.png -------------------------------------------------------------------------------- /public/swift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/swift.png -------------------------------------------------------------------------------- /public/ts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/ts.png -------------------------------------------------------------------------------- /public/typescript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/public/typescript.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(root)/_components/EditorPanel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useCodeEditorStore } from "@/store/useCodeEditorStore"; 3 | import { useEffect, useState } from "react"; 4 | import { defineMonacoThemes, LANGUAGE_CONFIG } from "../_constants"; 5 | import { Editor } from "@monaco-editor/react"; 6 | import { motion } from "framer-motion"; 7 | import Image from "next/image"; 8 | import { RotateCcwIcon, ShareIcon, TypeIcon } from "lucide-react"; 9 | import { useClerk } from "@clerk/nextjs"; 10 | import { EditorPanelSkeleton } from "./EditorPanelSkeleton"; 11 | import useMounted from "@/hooks/useMounted"; 12 | import ShareSnippetDialog from "./ShareSnippetDialog"; 13 | 14 | function EditorPanel() { 15 | const clerk = useClerk(); 16 | const [isShareDialogOpen, setIsShareDialogOpen] = useState(false); 17 | const { language, theme, fontSize, editor, setFontSize, setEditor } = useCodeEditorStore(); 18 | 19 | const mounted = useMounted(); 20 | 21 | useEffect(() => { 22 | const savedCode = localStorage.getItem(`editor-code-${language}`); 23 | const newCode = savedCode || LANGUAGE_CONFIG[language].defaultCode; 24 | if (editor) editor.setValue(newCode); 25 | }, [language, editor]); 26 | 27 | useEffect(() => { 28 | const savedFontSize = localStorage.getItem("editor-font-size"); 29 | if (savedFontSize) setFontSize(parseInt(savedFontSize)); 30 | }, [setFontSize]); 31 | 32 | const handleRefresh = () => { 33 | const defaultCode = LANGUAGE_CONFIG[language].defaultCode; 34 | if (editor) editor.setValue(defaultCode); 35 | localStorage.removeItem(`editor-code-${language}`); 36 | }; 37 | 38 | const handleEditorChange = (value: string | undefined) => { 39 | if (value) localStorage.setItem(`editor-code-${language}`, value); 40 | }; 41 | 42 | const handleFontSizeChange = (newSize: number) => { 43 | const size = Math.min(Math.max(newSize, 12), 24); 44 | setFontSize(size); 45 | localStorage.setItem("editor-font-size", size.toString()); 46 | }; 47 | 48 | if (!mounted) return null; 49 | 50 | return ( 51 |
52 |
53 | {/* Header */} 54 |
55 |
56 |
57 | Logo 58 |
59 |
60 |

Code Editor

61 |

Write and execute your code

62 |
63 |
64 |
65 | {/* Font Size Slider */} 66 |
67 | 68 |
69 | handleFontSizeChange(parseInt(e.target.value))} 75 | className="w-20 h-1 bg-gray-600 rounded-lg cursor-pointer" 76 | /> 77 | 78 | {fontSize} 79 | 80 |
81 |
82 | 83 | 90 | 91 | 92 | 93 | {/* Share Button */} 94 | setIsShareDialogOpen(true)} 98 | className="inline-flex items-center gap-2 px-4 py-2 rounded-lg overflow-hidden bg-gradient-to-r 99 | from-blue-500 to-blue-600 opacity-90 hover:opacity-100 transition-opacity" 100 | > 101 | 102 | Share 103 | 104 |
105 |
106 | 107 | {/* Editor */} 108 |
109 | {clerk.loaded && ( 110 | setEditor(editor)} 117 | options={{ 118 | minimap: { enabled: false }, 119 | fontSize, 120 | automaticLayout: true, 121 | scrollBeyondLastLine: false, 122 | padding: { top: 16, bottom: 16 }, 123 | renderWhitespace: "selection", 124 | fontFamily: '"Fira Code", "Cascadia Code", Consolas, monospace', 125 | fontLigatures: true, 126 | cursorBlinking: "smooth", 127 | smoothScrolling: true, 128 | contextmenu: true, 129 | renderLineHighlight: "all", 130 | lineHeight: 1.6, 131 | letterSpacing: 0.5, 132 | roundedSelection: true, 133 | scrollbar: { 134 | verticalScrollbarSize: 8, 135 | horizontalScrollbarSize: 8, 136 | }, 137 | }} 138 | /> 139 | )} 140 | 141 | {!clerk.loaded && } 142 |
143 |
144 | {isShareDialogOpen && setIsShareDialogOpen(false)} />} 145 |
146 | ); 147 | } 148 | export default EditorPanel; 149 | -------------------------------------------------------------------------------- /src/app/(root)/_components/EditorPanelSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Terminal } from "lucide-react"; 2 | 3 | export function EditorPanelSkeleton() { 4 | return ( 5 |
6 |
7 |
8 | {/* Editor Area Skeleton */} 9 |
10 |
11 |
12 | {/* Code line skeletons */} 13 | {[...Array(15)].map((_, i) => ( 14 |
15 |
16 |
20 |
21 | ))} 22 |
23 |
24 | 25 | {/* Bottom Bar */} 26 |
27 |
28 |
29 |
30 |
31 | ); 32 | } 33 | 34 | export function OutputPanelSkeleton() { 35 | return ( 36 |
37 | {/* Header */} 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 | {/* Output Area Skeleton */} 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | ); 61 | } 62 | 63 | // Loading state for the entire editor view 64 | export function EditorViewSkeleton() { 65 | return ( 66 |
67 | 68 | 69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/app/(root)/_components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs/server"; 2 | import { ConvexHttpClient } from "convex/browser"; 3 | import { api } from "../../../../convex/_generated/api"; 4 | import Link from "next/link"; 5 | import { Blocks, Code2, Sparkles } from "lucide-react"; 6 | import { SignedIn } from "@clerk/nextjs"; 7 | import ThemeSelector from "./ThemeSelector"; 8 | import LanguageSelector from "./LanguageSelector"; 9 | import RunButton from "./RunButton"; 10 | import HeaderProfileBtn from "./HeaderProfileBtn"; 11 | 12 | async function Header() { 13 | const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 14 | const user = await currentUser(); 15 | 16 | const convexUser = await convex.query(api.users.getUser, { 17 | userId: user?.id || "", 18 | }); 19 | 20 | return ( 21 |
22 |
26 |
27 | 28 | {/* Logo hover effect */} 29 | 30 |
34 | 35 | {/* Logo */} 36 |
40 | 41 |
42 | 43 |
44 | 45 | CodeCraft 46 | 47 | 48 | Interactive Code Editor 49 | 50 |
51 | 52 | 53 | {/* Navigation */} 54 | 73 |
74 | 75 |
76 |
77 | 78 | 79 |
80 | 81 | {!convexUser?.isPro && ( 82 | 88 | 89 | 90 | Pro 91 | 92 | 93 | )} 94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 |
102 |
103 |
104 |
105 | ); 106 | } 107 | export default Header; 108 | -------------------------------------------------------------------------------- /src/app/(root)/_components/HeaderProfileBtn.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import LoginButton from "@/components/LoginButton"; 3 | import { SignedOut, UserButton } from "@clerk/nextjs"; 4 | import { User } from "lucide-react"; 5 | 6 | function HeaderProfileBtn() { 7 | return ( 8 | <> 9 | 10 | 11 | } 14 | href="/profile" 15 | /> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | export default HeaderProfileBtn; 26 | -------------------------------------------------------------------------------- /src/app/(root)/_components/LanguageSelector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useCodeEditorStore } from "@/store/useCodeEditorStore"; 3 | import { useEffect, useRef, useState } from "react"; 4 | import { LANGUAGE_CONFIG } from "../_constants"; 5 | import { motion, AnimatePresence } from "framer-motion"; 6 | import Image from "next/image"; 7 | import { ChevronDownIcon, Lock, Sparkles } from "lucide-react"; 8 | import useMounted from "@/hooks/useMounted"; 9 | 10 | function LanguageSelector({ hasAccess }: { hasAccess: boolean }) { 11 | const [isOpen, setIsOpen] = useState(false); 12 | const mounted = useMounted(); 13 | 14 | const { language, setLanguage } = useCodeEditorStore(); 15 | const dropdownRef = useRef(null); 16 | const currentLanguageObj = LANGUAGE_CONFIG[language]; 17 | 18 | useEffect(() => { 19 | const handleClickOutside = (event: MouseEvent) => { 20 | if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { 21 | setIsOpen(false); 22 | } 23 | }; 24 | 25 | document.addEventListener("mousedown", handleClickOutside); 26 | return () => document.removeEventListener("mousedown", handleClickOutside); 27 | }, []); 28 | 29 | const handleLanguageSelect = (langId: string) => { 30 | if (!hasAccess && langId !== "javascript") return; 31 | 32 | setLanguage(langId); 33 | setIsOpen(false); 34 | }; 35 | 36 | if (!mounted) return null; 37 | 38 | return ( 39 |
40 | setIsOpen(!isOpen)} 44 | className={`group relative flex items-center gap-3 px-4 py-2.5 bg-[#1e1e2e]/80 45 | rounded-lg transition-all 46 | duration-200 border border-gray-800/50 hover:border-gray-700 47 | ${!hasAccess && language !== "javascript" ? "opacity-50 cursor-not-allowed" : ""}`} 48 | > 49 | {/* Decoration */} 50 | 168 | ); 169 | } 170 | export default LanguageSelector; 171 | -------------------------------------------------------------------------------- /src/app/(root)/_components/OutputPanel.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCodeEditorStore } from "@/store/useCodeEditorStore"; 4 | import { AlertTriangle, CheckCircle, Clock, Copy, Terminal } from "lucide-react"; 5 | import { useState } from "react"; 6 | import RunningCodeSkeleton from "./RunningCodeSkeleton"; 7 | 8 | function OutputPanel() { 9 | const { output, error, isRunning } = useCodeEditorStore(); 10 | const [isCopied, setIsCopied] = useState(false); 11 | 12 | const hasContent = error || output; 13 | 14 | const handleCopy = async () => { 15 | if (!hasContent) return; 16 | await navigator.clipboard.writeText(error || output); 17 | setIsCopied(true); 18 | 19 | setTimeout(() => setIsCopied(false), 2000); 20 | }; 21 | 22 | return ( 23 |
24 | {/* Header */} 25 |
26 |
27 |
28 | 29 |
30 | Output 31 |
32 | 33 | {hasContent && ( 34 | 51 | )} 52 |
53 | 54 | {/* Output Area */} 55 |
56 |
60 | {isRunning ? ( 61 | 62 | ) : error ? ( 63 |
64 | 65 |
66 |
Execution Error
67 |
{error}
68 |
69 |
70 | ) : output ? ( 71 |
72 |
73 | 74 | Execution Successful 75 |
76 |
{output}
77 |
78 | ) : ( 79 |
80 |
81 | 82 |
83 |

Run your code to see the output here...

84 |
85 | )} 86 |
87 |
88 |
89 | ); 90 | } 91 | 92 | export default OutputPanel; 93 | -------------------------------------------------------------------------------- /src/app/(root)/_components/RunButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { getExecutionResult, useCodeEditorStore } from "@/store/useCodeEditorStore"; 4 | import { useUser } from "@clerk/nextjs"; 5 | import { useMutation } from "convex/react"; 6 | import { motion } from "framer-motion"; 7 | import { Loader2, Play } from "lucide-react"; 8 | import { api } from "../../../../convex/_generated/api"; 9 | 10 | function RunButton() { 11 | const { user } = useUser(); 12 | const { runCode, language, isRunning } = useCodeEditorStore(); 13 | const saveExecution = useMutation(api.codeExecutions.saveExecution); 14 | 15 | const handleRun = async () => { 16 | await runCode(); 17 | const result = getExecutionResult(); 18 | 19 | if (user && result) { 20 | await saveExecution({ 21 | language, 22 | code: result.code, 23 | output: result.output || undefined, 24 | error: result.error || undefined, 25 | }); 26 | } 27 | }; 28 | 29 | return ( 30 | 41 | {/* bg wit gradient */} 42 |
43 | 44 |
45 | {isRunning ? ( 46 | <> 47 |
48 | 49 |
50 |
51 | Executing... 52 | 53 | ) : ( 54 | <> 55 |
56 | 57 |
58 | 59 | Run Code 60 | 61 | 62 | )} 63 |
64 | 65 | ); 66 | } 67 | export default RunButton; 68 | -------------------------------------------------------------------------------- /src/app/(root)/_components/RunningCodeSkeleton.tsx: -------------------------------------------------------------------------------- 1 | const RunningCodeSkeleton = () => ( 2 |
3 |
4 |
5 |
6 |
7 |
8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 | ); 16 | 17 | export default RunningCodeSkeleton; 18 | -------------------------------------------------------------------------------- /src/app/(root)/_components/ShareSnippetDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useCodeEditorStore } from "@/store/useCodeEditorStore"; 2 | import { useMutation } from "convex/react"; 3 | import { useState } from "react"; 4 | import { api } from "../../../../convex/_generated/api"; 5 | import { X } from "lucide-react"; 6 | import toast from "react-hot-toast"; 7 | 8 | function ShareSnippetDialog({ onClose }: { onClose: () => void }) { 9 | const [title, setTitle] = useState(""); 10 | const [isSharing, setIsSharing] = useState(false); 11 | const { language, getCode } = useCodeEditorStore(); 12 | const createSnippet = useMutation(api.snippets.createSnippet); 13 | 14 | const handleShare = async (e: React.FormEvent) => { 15 | e.preventDefault(); 16 | 17 | setIsSharing(true); 18 | 19 | try { 20 | const code = getCode(); 21 | await createSnippet({ title, language, code }); 22 | onClose(); 23 | setTitle(""); 24 | toast.success("Snippet shared successfully"); 25 | } catch (error) { 26 | console.log("Error creating snippet:", error); 27 | toast.error("Error creating snippet"); 28 | } finally { 29 | setIsSharing(false); 30 | } 31 | }; 32 | 33 | return ( 34 |
35 |
36 |
37 |

Share Snippet

38 | 41 |
42 | 43 |
44 |
45 | 48 | setTitle(e.target.value)} 53 | className="w-full px-3 py-2 bg-[#181825] border border-[#313244] rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500" 54 | placeholder="Enter snippet title" 55 | required 56 | /> 57 |
58 | 59 |
60 | 67 | 75 |
76 |
77 |
78 |
79 | ); 80 | } 81 | export default ShareSnippetDialog; 82 | -------------------------------------------------------------------------------- /src/app/(root)/_components/ThemeSelector.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCodeEditorStore } from "@/store/useCodeEditorStore"; 4 | import React, { useEffect, useRef, useState } from "react"; 5 | import { THEMES } from "../_constants"; 6 | import { AnimatePresence, motion } from "framer-motion"; 7 | import { CircleOff, Cloud, Github, Laptop, Moon, Palette, Sun } from "lucide-react"; 8 | import useMounted from "@/hooks/useMounted"; 9 | 10 | const THEME_ICONS: Record = { 11 | "vs-dark": , 12 | "vs-light": , 13 | "github-dark": , 14 | monokai: , 15 | "solarized-dark": , 16 | }; 17 | 18 | function ThemeSelector() { 19 | const [isOpen, setIsOpen] = useState(false); 20 | const mounted = useMounted(); 21 | const { theme, setTheme } = useCodeEditorStore(); 22 | const dropdownRef = useRef(null); 23 | const currentTheme = THEMES.find((t) => t.id === theme); 24 | 25 | useEffect(() => { 26 | const handleClickOutside = (event: MouseEvent) => { 27 | if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { 28 | setIsOpen(false); 29 | } 30 | }; 31 | 32 | document.addEventListener("mousedown", handleClickOutside); 33 | return () => document.removeEventListener("mousedown", handleClickOutside); 34 | }, []); 35 | 36 | if (!mounted) return null; 37 | 38 | return ( 39 |
40 | setIsOpen(!isOpen)} 44 | className="w-48 group relative flex items-center gap-2 px-4 py-2.5 bg-[#1e1e2e]/80 hover:bg-[#262637] 45 | rounded-lg transition-all duration-200 border border-gray-800/50 hover:border-gray-700" 46 | > 47 | {/* hover state bg decorator */} 48 |
49 | 50 | 51 | 52 | 53 | {currentTheme?.label} 54 | 55 | 56 | {/* color indicator */} 57 | 58 |
62 | 63 | 64 | 65 | {isOpen && ( 66 | 74 |
75 |

Select Theme

76 |
77 | 78 | {THEMES.map((t, index) => ( 79 | setTheme(t.id)} 89 | > 90 | {/* bg gradient */} 91 |
95 | 96 | {/* icon */} 97 |
104 | {THEME_ICONS[t.id] || } 105 |
106 | {/* label */} 107 | 108 | {t.label} 109 | 110 | 111 | {/* color indicator */} 112 |
117 | 118 | {/* active theme border */} 119 | {theme === t.id && ( 120 | 124 | )} 125 | 126 | ))} 127 | 128 | )} 129 | 130 |
131 | ); 132 | } 133 | export default ThemeSelector; 134 | -------------------------------------------------------------------------------- /src/app/(root)/_constants/index.ts: -------------------------------------------------------------------------------- 1 | import { Monaco } from "@monaco-editor/react"; 2 | import { Theme } from "../../../types"; 3 | 4 | type LanguageConfig = Record< 5 | string, 6 | { 7 | id: string; 8 | label: string; 9 | logoPath: string; 10 | pistonRuntime: { language: string; version: string }; 11 | monacoLanguage: string; 12 | defaultCode: string; 13 | } 14 | >; 15 | 16 | export const LANGUAGE_CONFIG: LanguageConfig = { 17 | javascript: { 18 | id: "javascript", 19 | label: "JavaScript", 20 | logoPath: "/javascript.png", 21 | pistonRuntime: { language: "javascript", version: "18.15.0" }, // api that we're gonna be using 22 | monacoLanguage: "javascript", 23 | defaultCode: `// JavaScript Playground 24 | const numbers = [1, 2, 3, 4, 5]; 25 | 26 | // Map numbers to their squares 27 | const squares = numbers.map(n => n * n); 28 | console.log('Original numbers:', numbers); 29 | console.log('Squared numbers:', squares); 30 | 31 | // Filter for even numbers 32 | const evenNumbers = numbers.filter(n => n % 2 === 0); 33 | console.log('Even numbers:', evenNumbers); 34 | 35 | // Calculate sum using reduce 36 | const sum = numbers.reduce((acc, curr) => acc + curr, 0); 37 | console.log('Sum of numbers:', sum);`, 38 | }, 39 | typescript: { 40 | id: "typescript", 41 | label: "TypeScript", 42 | logoPath: "/typescript.png", 43 | pistonRuntime: { language: "typescript", version: "5.0.3" }, 44 | monacoLanguage: "typescript", 45 | defaultCode: `// TypeScript Playground 46 | interface NumberArray { 47 | numbers: number[]; 48 | sum(): number; 49 | squares(): number[]; 50 | evenNumbers(): number[]; 51 | } 52 | 53 | class MathOperations implements NumberArray { 54 | constructor(public numbers: number[]) {} 55 | 56 | sum(): number { 57 | return this.numbers.reduce((acc, curr) => acc + curr, 0); 58 | } 59 | 60 | squares(): number[] { 61 | return this.numbers.map(n => n * n); 62 | } 63 | 64 | evenNumbers(): number[] { 65 | return this.numbers.filter(n => n % 2 === 0); 66 | } 67 | } 68 | 69 | const math = new MathOperations([1, 2, 3, 4, 5]); 70 | 71 | console.log('Original numbers:', math.numbers); 72 | console.log('Squared numbers:', math.squares()); 73 | console.log('Even numbers:', math.evenNumbers()); 74 | console.log('Sum of numbers:', math.sum());`, 75 | }, 76 | python: { 77 | id: "python", 78 | label: "Python", 79 | logoPath: "/python.png", 80 | pistonRuntime: { language: "python", version: "3.10.0" }, 81 | monacoLanguage: "python", 82 | defaultCode: `# Python Playground 83 | numbers = [1, 2, 3, 4, 5] 84 | 85 | # Map numbers to their squares 86 | squares = [n ** 2 for n in numbers] 87 | print(f"Original numbers: {numbers}") 88 | print(f"Squared numbers: {squares}") 89 | 90 | # Filter for even numbers 91 | even_numbers = [n for n in numbers if n % 2 == 0] 92 | print(f"Even numbers: {even_numbers}") 93 | 94 | # Calculate sum 95 | numbers_sum = sum(numbers) 96 | print(f"Sum of numbers: {numbers_sum}")`, 97 | }, 98 | java: { 99 | id: "java", 100 | label: "Java", 101 | logoPath: "/java.png", 102 | pistonRuntime: { language: "java", version: "15.0.2" }, 103 | monacoLanguage: "java", 104 | defaultCode: `public class Main { 105 | public static void main(String[] args) { 106 | // Create array 107 | int[] numbers = {1, 2, 3, 4, 5}; 108 | 109 | // Print original numbers 110 | System.out.print("Original numbers: "); 111 | printArray(numbers); 112 | 113 | // Calculate and print squares 114 | int[] squares = new int[numbers.length]; 115 | for (int i = 0; i < numbers.length; i++) { 116 | squares[i] = numbers[i] * numbers[i]; 117 | } 118 | System.out.print("Squared numbers: "); 119 | printArray(squares); 120 | 121 | // Print even numbers 122 | System.out.print("Even numbers: "); 123 | for (int n : numbers) { 124 | if (n % 2 == 0) System.out.print(n + " "); 125 | } 126 | System.out.println(); 127 | 128 | // Calculate and print sum 129 | int sum = 0; 130 | for (int n : numbers) sum += n; 131 | System.out.println("Sum of numbers: " + sum); 132 | } 133 | 134 | private static void printArray(int[] arr) { 135 | for (int n : arr) System.out.print(n + " "); 136 | System.out.println(); 137 | } 138 | }`, 139 | }, 140 | go: { 141 | id: "go", 142 | label: "Go", 143 | logoPath: "/go.png", 144 | pistonRuntime: { language: "go", version: "1.16.2" }, 145 | monacoLanguage: "go", 146 | defaultCode: `package main 147 | 148 | import "fmt" 149 | 150 | func main() { 151 | // Create slice 152 | numbers := []int{1, 2, 3, 4, 5} 153 | 154 | // Print original numbers 155 | fmt.Println("Original numbers:", numbers) 156 | 157 | // Calculate squares 158 | squares := make([]int, len(numbers)) 159 | for i, n := range numbers { 160 | squares[i] = n * n 161 | } 162 | fmt.Println("Squared numbers:", squares) 163 | 164 | // Filter even numbers 165 | var evenNumbers []int 166 | for _, n := range numbers { 167 | if n%2 == 0 { 168 | evenNumbers = append(evenNumbers, n) 169 | } 170 | } 171 | fmt.Println("Even numbers:", evenNumbers) 172 | 173 | // Calculate sum 174 | sum := 0 175 | for _, n := range numbers { 176 | sum += n 177 | } 178 | fmt.Println("Sum of numbers:", sum) 179 | }`, 180 | }, 181 | rust: { 182 | id: "rust", 183 | label: "Rust", 184 | logoPath: "/rust.png", 185 | pistonRuntime: { language: "rust", version: "1.68.2" }, 186 | monacoLanguage: "rust", 187 | defaultCode: `fn main() { 188 | // Create vector 189 | let numbers = vec![1, 2, 3, 4, 5]; 190 | 191 | // Print original numbers 192 | println!("Original numbers: {:?}", numbers); 193 | 194 | // Calculate squares 195 | let squares: Vec = numbers 196 | .iter() 197 | .map(|&n| n * n) 198 | .collect(); 199 | println!("Squared numbers: {:?}", squares); 200 | 201 | // Filter even numbers 202 | let even_numbers: Vec = numbers 203 | .iter() 204 | .filter(|&&n| n % 2 == 0) 205 | .cloned() 206 | .collect(); 207 | println!("Even numbers: {:?}", even_numbers); 208 | 209 | // Calculate sum 210 | let sum: i32 = numbers.iter().sum(); 211 | println!("Sum of numbers: {}", sum); 212 | }`, 213 | }, 214 | cpp: { 215 | id: "cpp", 216 | label: "C++", 217 | logoPath: "/cpp.png", 218 | pistonRuntime: { language: "cpp", version: "10.2.0" }, 219 | monacoLanguage: "cpp", 220 | defaultCode: `#include 221 | #include 222 | #include 223 | #include 224 | 225 | int main() { 226 | // Create vector 227 | std::vector numbers = {1, 2, 3, 4, 5}; 228 | 229 | // Print original numbers 230 | std::cout << "Original numbers: "; 231 | for (int n : numbers) std::cout << n << " "; 232 | std::cout << std::endl; 233 | 234 | // Calculate squares 235 | std::vector squares; 236 | std::transform(numbers.begin(), numbers.end(), 237 | std::back_inserter(squares), 238 | [](int n) { return n * n; }); 239 | 240 | std::cout << "Squared numbers: "; 241 | for (int n : squares) std::cout << n << " "; 242 | std::cout << std::endl; 243 | 244 | // Filter even numbers 245 | std::cout << "Even numbers: "; 246 | for (int n : numbers) { 247 | if (n % 2 == 0) std::cout << n << " "; 248 | } 249 | std::cout << std::endl; 250 | 251 | // Calculate sum 252 | int sum = std::accumulate(numbers.begin(), numbers.end(), 0); 253 | std::cout << "Sum of numbers: " << sum << std::endl; 254 | 255 | return 0; 256 | }`, 257 | }, 258 | csharp: { 259 | id: "csharp", 260 | label: "C#", 261 | logoPath: "/csharp.png", 262 | pistonRuntime: { language: "csharp", version: "6.12.0" }, 263 | monacoLanguage: "csharp", 264 | defaultCode: `using System; 265 | using System.Linq; 266 | 267 | class Program { 268 | static void Main() { 269 | // Create array 270 | int[] numbers = { 1, 2, 3, 4, 5 }; 271 | 272 | // Print original numbers 273 | Console.WriteLine($"Original numbers: {string.Join(" ", numbers)}"); 274 | 275 | // Calculate squares 276 | var squares = numbers.Select(n => n * n); 277 | Console.WriteLine($"Squared numbers: {string.Join(" ", squares)}"); 278 | 279 | // Filter even numbers 280 | var evenNumbers = numbers.Where(n => n % 2 == 0); 281 | Console.WriteLine($"Even numbers: {string.Join(" ", evenNumbers)}"); 282 | 283 | // Calculate sum 284 | var sum = numbers.Sum(); 285 | Console.WriteLine($"Sum of numbers: {sum}"); 286 | } 287 | }`, 288 | }, 289 | ruby: { 290 | id: "ruby", 291 | label: "Ruby", 292 | logoPath: "/ruby.png", 293 | pistonRuntime: { language: "ruby", version: "3.0.1" }, 294 | monacoLanguage: "ruby", 295 | defaultCode: `# Create array 296 | numbers = [1, 2, 3, 4, 5] 297 | 298 | # Print original numbers 299 | puts "Original numbers: #{numbers.join(' ')}" 300 | 301 | # Calculate squares 302 | squares = numbers.map { |n| n * n } 303 | puts "Squared numbers: #{squares.join(' ')}" 304 | 305 | # Filter even numbers 306 | even_numbers = numbers.select { |n| n.even? } 307 | puts "Even numbers: #{even_numbers.join(' ')}" 308 | 309 | # Calculate sum 310 | sum = numbers.sum 311 | puts "Sum of numbers: #{sum}"`, 312 | }, 313 | swift: { 314 | id: "swift", 315 | label: "Swift", 316 | logoPath: "/swift.png", 317 | pistonRuntime: { language: "swift", version: "5.3.3" }, 318 | monacoLanguage: "swift", 319 | defaultCode: `// Create array 320 | let numbers = [1, 2, 3, 4, 5] 321 | 322 | // Print original numbers 323 | print("Original numbers: \\(numbers)") 324 | 325 | // Calculate squares 326 | let squares = numbers.map { $0 * $0 } 327 | print("Squared numbers: \\(squares)") 328 | 329 | // Filter even numbers 330 | let evenNumbers = numbers.filter { $0 % 2 == 0 } 331 | print("Even numbers: \\(evenNumbers)") 332 | 333 | // Calculate sum 334 | let sum = numbers.reduce(0, +) 335 | print("Sum of numbers: \\(sum)")`, 336 | }, 337 | }; 338 | 339 | export const THEMES: Theme[] = [ 340 | { id: "vs-dark", label: "VS Dark", color: "#1e1e1e" }, 341 | { id: "vs-light", label: "VS Light", color: "#ffffff" }, 342 | { id: "github-dark", label: "GitHub Dark", color: "#0d1117" }, 343 | { id: "monokai", label: "Monokai", color: "#272822" }, 344 | { id: "solarized-dark", label: "Solarized Dark", color: "#002b36" }, 345 | ]; 346 | 347 | export const THEME_DEFINITONS = { 348 | "github-dark": { 349 | base: "vs-dark", 350 | inherit: true, 351 | rules: [ 352 | { token: "comment", foreground: "6e7681" }, 353 | { token: "string", foreground: "a5d6ff" }, 354 | { token: "keyword", foreground: "ff7b72" }, 355 | { token: "number", foreground: "79c0ff" }, 356 | { token: "type", foreground: "ffa657" }, 357 | { token: "class", foreground: "ffa657" }, 358 | { token: "function", foreground: "d2a8ff" }, 359 | { token: "variable", foreground: "ffa657" }, 360 | { token: "operator", foreground: "ff7b72" }, 361 | ], 362 | colors: { 363 | "editor.background": "#0d1117", 364 | "editor.foreground": "#c9d1d9", 365 | "editor.lineHighlightBackground": "#161b22", 366 | "editorLineNumber.foreground": "#6e7681", 367 | "editorIndentGuide.background": "#21262d", 368 | "editor.selectionBackground": "#264f78", 369 | "editor.inactiveSelectionBackground": "#264f7855", 370 | }, 371 | }, 372 | monokai: { 373 | base: "vs-dark", 374 | inherit: true, 375 | rules: [ 376 | { token: "comment", foreground: "75715E" }, 377 | { token: "string", foreground: "E6DB74" }, 378 | { token: "keyword", foreground: "F92672" }, 379 | { token: "number", foreground: "AE81FF" }, 380 | { token: "type", foreground: "66D9EF" }, 381 | { token: "class", foreground: "A6E22E" }, 382 | { token: "function", foreground: "A6E22E" }, 383 | { token: "variable", foreground: "F8F8F2" }, 384 | { token: "operator", foreground: "F92672" }, 385 | ], 386 | colors: { 387 | "editor.background": "#272822", 388 | "editor.foreground": "#F8F8F2", 389 | "editorLineNumber.foreground": "#75715E", 390 | "editor.selectionBackground": "#49483E", 391 | "editor.lineHighlightBackground": "#3E3D32", 392 | "editorCursor.foreground": "#F8F8F2", 393 | "editor.selectionHighlightBackground": "#49483E", 394 | }, 395 | }, 396 | "solarized-dark": { 397 | base: "vs-dark", 398 | inherit: true, 399 | rules: [ 400 | { token: "comment", foreground: "586e75" }, 401 | { token: "string", foreground: "2aa198" }, 402 | { token: "keyword", foreground: "859900" }, 403 | { token: "number", foreground: "d33682" }, 404 | { token: "type", foreground: "b58900" }, 405 | { token: "class", foreground: "b58900" }, 406 | { token: "function", foreground: "268bd2" }, 407 | { token: "variable", foreground: "b58900" }, 408 | { token: "operator", foreground: "859900" }, 409 | ], 410 | colors: { 411 | "editor.background": "#002b36", 412 | "editor.foreground": "#839496", 413 | "editorLineNumber.foreground": "#586e75", 414 | "editor.selectionBackground": "#073642", 415 | "editor.lineHighlightBackground": "#073642", 416 | "editorCursor.foreground": "#839496", 417 | "editor.selectionHighlightBackground": "#073642", 418 | }, 419 | }, 420 | }; 421 | 422 | // Helper function to define themes in Monaco 423 | export const defineMonacoThemes = (monaco: Monaco) => { 424 | Object.entries(THEME_DEFINITONS).forEach(([themeName, themeData]) => { 425 | monaco.editor.defineTheme(themeName, { 426 | base: themeData.base, 427 | inherit: themeData.inherit, 428 | rules: themeData.rules.map((rule) => ({ 429 | ...rule, 430 | foreground: rule.foreground, 431 | })), 432 | colors: themeData.colors, 433 | }); 434 | }); 435 | }; 436 | -------------------------------------------------------------------------------- /src/app/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | import EditorPanel from "./_components/EditorPanel"; 2 | import Header from "./_components/Header"; 3 | import OutputPanel from "./_components/OutputPanel"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/burakorkmez/code-craft/d821d33f44b6aa9db5f98d8caa5e2c9970f315f6/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import ConvexClientProvider from "@/components/providers/ConvexClientProvider"; 6 | import Footer from "@/components/Footer"; 7 | import { Toaster } from "react-hot-toast"; 8 | 9 | const geistSans = localFont({ 10 | src: "./fonts/GeistVF.woff", 11 | variable: "--font-geist-sans", 12 | weight: "100 900", 13 | }); 14 | const geistMono = localFont({ 15 | src: "./fonts/GeistMonoVF.woff", 16 | variable: "--font-geist-mono", 17 | weight: "100 900", 18 | }); 19 | 20 | export const metadata: Metadata = { 21 | title: "Code Craft", 22 | description: "Share and run code snippets", 23 | }; 24 | 25 | export default function RootLayout({ 26 | children, 27 | }: Readonly<{ 28 | children: React.ReactNode; 29 | }>) { 30 | return ( 31 | 32 | 33 | 36 | {children} 37 | 38 |