Compare commits

..

28 Commits

Author SHA1 Message Date
0828209455 Chore: Change about to help, and add link to Discord 2024-11-17 18:10:19 -05:00
94ca5b2c9f fix: comment out live collaboration features 2024-11-17 17:52:39 -05:00
2a58d0a5e3 fix: type errors, shared page avatars and project icons 2024-11-11 16:02:26 -05:00
30c9da559f feat: user avatar images
- added user avatars for each user
- it will fetch user images from github or google and if there is no image then it will show initials
2024-11-11 16:01:47 -05:00
2262adca74 feat: schema updates
- added additional items to users and sandbox tables
- added a random username generator
2024-11-10 21:52:52 -05:00
b486d22111 chore: remove unnecessary logs 2024-11-09 17:57:48 -05:00
81399cd351 docs: update README 2024-11-08 14:42:53 -05:00
96812027d0 docs: add custom template instructions 2024-11-04 17:57:25 -06:00
85abbbdb0b fix: ignore certains files and folders from the file tree
- Created new config file for ignored paths in file system traversal
- Separated ignored folders and files into dedicated arrays
- Includes comprehensive ignore patterns for:
  - Package managers (node_modules, venv)
  - Build outputs and caches
  - Version control
  - IDE specific folders
  - Framework specific directories
  - System and config files
  - Lock files and compiled assets
2024-11-04 17:53:34 -05:00
3db3fbc490 chore: delete unused files 2024-11-04 17:30:13 -05:00
c6c01101f1 feat: multi-file context, context tabs
- added context tabs
- added multifile context including file and image uploads to the context along with all the files from the project
- added file/image previews on input
- added code paste from the editor and file lines recognition
- added image paste from clipboard and preview
2024-11-04 17:29:23 -05:00
9c6067dcd9 feat: enhance AI Chat with context management, file integration, image support, and improved code handling
- Added context tabs system for managing multiple types of context (files, code snippets, images)
   - Added preview functionality for context items
   - Added ability to expand/collapse context previews
   - Added file selection popup/dropdown
   - Added file search functionality
   - Added image upload button
   - Added image paste support
   - Added image preview in context tabs
   - Added automatic code detection on paste
   - Added line number tracking for code snippets
   - Added source file name preservation
   - Added line range display for code contexts
   - Added model selection dropdown (Claude 3.5 Sonnet/Claude 3)
   - Added Ctrl+Enter for sending with full context
   - Added Backspace to remove last context tab when input is empty
   - Added smart code detection on paste
2024-11-04 17:24:22 -05:00
9c98e41ebb chore: delete unused files 2024-11-03 12:50:58 -06:00
c669babb2f fix: use posix paths when converting paths to relative format 2024-11-03 12:28:17 -06:00
474102aa14 fix: use new project directory path to find tsconfig files 2024-11-02 13:28:48 -06:00
5a63ab7265 feat: load project templates from custom E2B sandboxes instead of from Cloudflare 2024-11-02 13:28:48 -06:00
5633727bdb chore: update template types 2024-11-02 13:28:48 -06:00
39911e9ef2 fix: add jszip to frontend 2024-11-02 05:55:50 -06:00
f35330ba4f chore: add missing entries to .env.example 2024-11-02 05:55:50 -06:00
9197050ca3 feat: add name of the project 2024-11-02 05:55:50 -06:00
ebb270911b fix: add jszip 2024-11-02 05:55:50 -06:00
60c5345753 feat: add download button 2024-11-02 05:55:31 -06:00
95154af074 docs: add note about Cloudflare Worker URLs 2024-11-01 07:59:35 -06:00
7ed2d14435 chore: changing the links 2024-10-27 17:27:55 -04:00
198b59aa55 feat: add AI chat button to open it 2024-10-27 17:27:36 -04:00
f6077ed516 fix: jsx.tolowercase error 2024-10-27 17:27:08 -04:00
684ee20a03 chore: removing unnecessary code 2024-10-27 17:26:43 -04:00
e658a84a9b chore: add posix to fix file not found errors 2024-10-27 17:25:21 -04:00
48 changed files with 11069 additions and 10910 deletions

View File

@ -11,7 +11,6 @@ For the latest updates, join our Discord server: [discord.gitwit.dev](https://di
Notes: Notes:
- Double check that whatever you change "SUPERDUPERSECRET" to, it's the same in all config files. - Double check that whatever you change "SUPERDUPERSECRET" to, it's the same in all config files.
- Right now we are loading project templates from a custom Cloudflare bucket which isn't covered in this guide, but that be updated/fixed very soon.
### 0. Requirements ### 0. Requirements
@ -197,13 +196,48 @@ DOKKU_KEY=
## Creating Custom Templates ## Creating Custom Templates
We're working on a process whereby anyone can contribute a custom template that others can use in the Sandbox environment. The process includes: Anyone can contribute a custom template for integration in Sandbox. Since Sandbox is built on E2B, there is no limitation to what langauge or runtime a Sandbox can use.
- Creating a [custom E2B Sandbox](https://e2b.dev/docs/sandbox-template) including the template files and dependencies Currently there are four templates:
- Creating a file to specify the run command (e.g. "npm run dev") - [jamesmurdza/dokku-reactjs-template](https://github.com/jamesmurdza/dokku-reactjs-template)
- Testing the template with Dokku for deployment - [jamesmurdza/dokku-vanillajs-template](https://github.com/jamesmurdza/dokku-vanillajs-template)
- [jamesmurdza/dokku-nextjs-template](https://github.com/jamesmurdza/dokku-nextjs-template)
- [jamesmurdza/dokku-streamlit-template](https://github.com/jamesmurdza/dokku-streamlit-template)
Please reach out to us [on Discord](https://discord.gitwit.dev/) if you're interested in contributing. To create your own template, you can fork one of the above templates or start with a new blank repository. The template should have at least an `e2b.Dockerfile`, which is used by E2B to create the development environment. Optionally, a `Dockerfile` can be added which will be used to create the project build when it is deployed.
To test the template, you must have an [E2B account](https://e2b.dev/) and the [E2B CLI tools](https://e2b.dev/docs/cli) installed. Then, in the Terminal, run:
```
e2b auth login
```
Then, navigate to your template directory and run the following command where **TEMPLATENAME** is the name of your template:
```
e2b template build -d e2b.Dockerfile -n TEMPLATENAME
```
Finally, to test your template run:
```
e2b sandbox spawn TEMPLATENAME
cd project
```
You will see a URL in the form of `https://xxxxxxxxxxxxxxxxxxx.e2b-staging.com`.
Now, run the command to start your development server.
To see the running server, visit the public url `https://<PORT>-xxxxxxxxxxxxxxxxxxx.e2b-staging.com`.
If you've done this and it works, let us know and we'll add your template to Sandbox! Please reach out to us [on Discord](https://discord.gitwit.dev/) with any questions or to submit your working template.
Note: In the future, we will add a way to specify the command triggered by the "Run" button (e.g. "npm run dev").
For more information, see:
- [Custom E2B Sandboxes](https://e2b.dev/docs/sandbox-template)
- [Dokku Builders](https://dokku.com/docs/deployment/builders/builder-management/)
## Contributing ## Contributing

View File

@ -1,7 +1,7 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "6570ba20-a672-400c-8147-7ba533784918", "id": "afe10bff-362b-402c-bdb5-038341692f35",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"sandbox": { "sandbox": {
@ -35,12 +35,36 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"likeCount": {
"name": "likeCount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"viewCount": {
"name": "viewCount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {
@ -93,6 +117,43 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatarUrl": {
"name": "avatarUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"generations": {
"name": "generations",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {
@ -102,6 +163,13 @@
"id" "id"
], ],
"isUnique": true "isUnique": true
},
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
} }
}, },
"foreignKeys": {}, "foreignKeys": {},
@ -124,6 +192,13 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"sharedOn": {
"name": "sharedOn",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
} }
}, },
"indexes": {}, "indexes": {},

View File

@ -1,8 +1,8 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "9f64104a-4954-40c0-8155-17755ea0a243", "id": "e570d5ac-700d-4e62-8a46-482b21ae1fe1",
"prevId": "6570ba20-a672-400c-8147-7ba533784918", "prevId": "afe10bff-362b-402c-bdb5-038341692f35",
"tables": { "tables": {
"sandbox": { "sandbox": {
"name": "sandbox", "name": "sandbox",
@ -35,12 +35,36 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"user_id": { "user_id": {
"name": "user_id", "name": "user_id",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"likeCount": {
"name": "likeCount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"viewCount": {
"name": "viewCount",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {
@ -94,12 +118,35 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"image": { "username": {
"name": "image", "name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatarUrl": {
"name": "avatarUrl",
"type": "text", "type": "text",
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"generations": {
"name": "generations",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {
@ -109,6 +156,13 @@
"id" "id"
], ],
"isUnique": true "isUnique": true
},
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
} }
}, },
"foreignKeys": {}, "foreignKeys": {},
@ -131,6 +185,13 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"sharedOn": {
"name": "sharedOn",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
} }
}, },
"indexes": {}, "indexes": {},

View File

@ -1,168 +0,0 @@
{
"version": "5",
"dialect": "sqlite",
"id": "5baf10d6-7697-42ba-a11a-ee4c7bd7e91e",
"prevId": "9f64104a-4954-40c0-8155-17755ea0a243",
"tables": {
"sandbox": {
"name": "sandbox",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"sandbox_id_unique": {
"name": "sandbox_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {
"sandbox_user_id_user_id_fk": {
"name": "sandbox_user_id_user_id_fk",
"tableFrom": "sandbox",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"user_id_unique": {
"name": "user_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users_to_sandboxes": {
"name": "users_to_sandboxes",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sandboxId": {
"name": "sandboxId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_to_sandboxes_userId_user_id_fk": {
"name": "users_to_sandboxes_userId_user_id_fk",
"tableFrom": "users_to_sandboxes",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"users_to_sandboxes_sandboxId_sandbox_id_fk": {
"name": "users_to_sandboxes_sandboxId_sandbox_id_fk",
"tableFrom": "users_to_sandboxes",
"tableTo": "sandbox",
"columnsFrom": [
"sandboxId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -1,175 +0,0 @@
{
"version": "5",
"dialect": "sqlite",
"id": "37e38b82-1494-4818-8c26-b9024cce3fa9",
"prevId": "5baf10d6-7697-42ba-a11a-ee4c7bd7e91e",
"tables": {
"sandbox": {
"name": "sandbox",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"visibility": {
"name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"sandbox_id_unique": {
"name": "sandbox_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {
"sandbox_user_id_user_id_fk": {
"name": "sandbox_user_id_user_id_fk",
"tableFrom": "sandbox",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_id_unique": {
"name": "user_id_unique",
"columns": [
"id"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users_to_sandboxes": {
"name": "users_to_sandboxes",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sandboxId": {
"name": "sandboxId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"users_to_sandboxes_userId_user_id_fk": {
"name": "users_to_sandboxes_userId_user_id_fk",
"tableFrom": "users_to_sandboxes",
"tableTo": "user",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"users_to_sandboxes_sandboxId_sandbox_id_fk": {
"name": "users_to_sandboxes_sandboxId_sandbox_id_fk",
"tableFrom": "users_to_sandboxes",
"tableTo": "sandbox",
"columnsFrom": [
"sandboxId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -5,50 +5,29 @@
{ {
"idx": 0, "idx": 0,
"version": "5", "version": "5",
"when": 1714540200800, "when": 1731288423588,
"tag": "0000_big_rogue", "tag": "0000_cuddly_patriot",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "5", "version": "5",
"when": 1714541190588, "when": 1731290863632,
"tag": "0001_empty_black_knight", "tag": "0001_opposite_newton_destine",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 2, "idx": 2,
"version": "5", "version": "5",
"when": 1714541209173, "when": 1731296235880,
"tag": "0002_sour_ego", "tag": "0002_rainy_fantastic_four",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 3, "idx": 3,
"version": "5", "version": "5",
"when": 1714541233589, "when": 1731297339306,
"tag": "0003_pale_overlord", "tag": "0003_lying_snowbird",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1714565073180,
"tag": "0004_cuddly_wolf_cub",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1714950365718,
"tag": "0005_last_the_twelve",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1716432225404,
"tag": "0006_lively_mattie_franklin",
"breakpoints": true "breakpoints": true
} }
] ]

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@
"drizzle-kit": "^0.20.17", "drizzle-kit": "^0.20.17",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitest": "1.3.0", "vitest": "1.3.0",
"wrangler": "^3.0.0" "wrangler": "^3.86.0"
}, },
"dependencies": { "dependencies": {
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
@ -29,4 +29,4 @@
"itty-router-extras": "^0.4.6", "itty-router-extras": "^0.4.6",
"zod": "^3.22.4" "zod": "^3.22.4"
} }
} }

View File

@ -169,6 +169,7 @@ export default {
name: sb.name, name: sb.name,
type: sb.type, type: sb.type,
author: sb.author.name, author: sb.author.name,
authorAvatarUrl: sb.author.avatarUrl,
sharedOn: r.sharedOn, sharedOn: r.sharedOn,
} }
}) })
@ -282,14 +283,26 @@ export default {
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
email: z.string().email(), email: z.string().email(),
username: z.string(),
avatarUrl: z.string().optional(),
createdAt: z.string().optional(),
generations: z.number().optional(),
}) })
const body = await request.json() const body = await request.json()
const { id, name, email } = userSchema.parse(body) const { id, name, email, username, avatarUrl, createdAt, generations } = userSchema.parse(body)
const res = await db const res = await db
.insert(user) .insert(user)
.values({ id, name, email }) .values({
id,
name,
email,
username,
avatarUrl,
createdAt: createdAt ? new Date(createdAt) : new Date(),
generations,
})
.returning() .returning()
.get() .get()
return json({ res }) return json({ res })
@ -303,6 +316,20 @@ export default {
} else { } else {
return methodNotAllowed return methodNotAllowed
} }
} else if (path === "/api/user/check-username") {
if (method === "GET") {
const params = url.searchParams
const username = params.get("username")
if (!username) return invalidRequest
const exists = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.username, username)
})
return json({ exists: !!exists })
}
return methodNotAllowed
} else return notFound } else return notFound
}, },
} }

View File

@ -1,6 +1,7 @@
import { createId } from "@paralleldrive/cuid2" import { createId } from "@paralleldrive/cuid2"
import { relations } from "drizzle-orm" import { relations } from "drizzle-orm"
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
import { sql } from "drizzle-orm"
export const user = sqliteTable("user", { export const user = sqliteTable("user", {
id: text("id") id: text("id")
@ -9,7 +10,10 @@ export const user = sqliteTable("user", {
.unique(), .unique(),
name: text("name").notNull(), name: text("name").notNull(),
email: text("email").notNull(), email: text("email").notNull(),
image: text("image"), username: text("username").notNull().unique(),
avatarUrl: text("avatarUrl"),
createdAt: integer("createdAt", { mode: "timestamp_ms" })
.default(sql`CURRENT_TIMESTAMP`),
generations: integer("generations").default(0), generations: integer("generations").default(0),
}) })
@ -28,10 +32,13 @@ export const sandbox = sqliteTable("sandbox", {
name: text("name").notNull(), name: text("name").notNull(),
type: text("type").notNull(), type: text("type").notNull(),
visibility: text("visibility", { enum: ["public", "private"] }), visibility: text("visibility", { enum: ["public", "private"] }),
createdAt: integer("createdAt", { mode: "timestamp_ms" }), createdAt: integer("createdAt", { mode: "timestamp_ms" })
.default(sql`CURRENT_TIMESTAMP`),
userId: text("user_id") userId: text("user_id")
.notNull() .notNull()
.references(() => user.id), .references(() => user.id),
likeCount: integer("likeCount").default(0),
viewCount: integer("viewCount").default(0),
}) })
export type Sandbox = typeof sandbox.$inferSelect export type Sandbox = typeof sandbox.$inferSelect

View File

@ -7,6 +7,7 @@ PORT=4000
WORKERS_KEY= WORKERS_KEY=
DATABASE_WORKER_URL= DATABASE_WORKER_URL=
STORAGE_WORKER_URL= STORAGE_WORKER_URL=
AI_WORKER_URL=
E2B_API_KEY= E2B_API_KEY=
DOKKU_HOST= DOKKU_HOST=
DOKKU_USERNAME= DOKKU_USERNAME=

View File

@ -140,6 +140,12 @@ export class FileManager {
}) })
await Promise.all(promises) await Promise.all(promises)
// Reload file list from the container to include template files
const result = await this.sandbox.commands.run(`find "${this.dirName}" -type f`); // List all files recursively
const localPaths = result.stdout.split('\n').filter(path => path); // Split the output into an array and filter out empty strings
const relativePaths = localPaths.map(filePath => path.posix.relative(this.dirName, filePath)); // Convert absolute paths to relative paths
this.files = generateFileStructure(relativePaths);
// Make the logged in user the owner of all project files // Make the logged in user the owner of all project files
this.fixPermissions() this.fixPermissions()
@ -348,8 +354,9 @@ export class FileManager {
// Get file content // Get file content
async getFile(fileId: string): Promise<string | undefined> { async getFile(fileId: string): Promise<string | undefined> {
const file = this.fileData.find((f) => f.id === fileId) const filePath = path.posix.join(this.dirName, fileId)
return file?.data const fileContent = await this.sandbox.files.read(filePath)
return fileContent
} }
// Get folder content // Get folder content

View File

@ -13,9 +13,8 @@ import {
} from "./ratelimit" } from "./ratelimit"
import { SecureGitClient } from "./SecureGitClient" import { SecureGitClient } from "./SecureGitClient"
import { TerminalManager } from "./TerminalManager" import { TerminalManager } from "./TerminalManager"
import { TFile, TFolder } from "./types" import { TFile, TFileData, TFolder } from "./types"
import { LockManager } from "./utils" import { LockManager } from "./utils"
const lockManager = new LockManager() const lockManager = new LockManager()
// Define a type for SocketHandler functions // Define a type for SocketHandler functions
@ -38,6 +37,7 @@ type ServerContext = {
export class Sandbox { export class Sandbox {
// Sandbox properties: // Sandbox properties:
sandboxId: string; sandboxId: string;
type: string;
fileManager: FileManager | null; fileManager: FileManager | null;
terminalManager: TerminalManager | null; terminalManager: TerminalManager | null;
container: E2BSandbox | null; container: E2BSandbox | null;
@ -46,9 +46,10 @@ export class Sandbox {
gitClient: SecureGitClient | null; gitClient: SecureGitClient | null;
aiWorker: AIWorker; aiWorker: AIWorker;
constructor(sandboxId: string, { aiWorker, dokkuClient, gitClient }: ServerContext) { constructor(sandboxId: string, type: string, { aiWorker, dokkuClient, gitClient }: ServerContext) {
// Sandbox properties: // Sandbox properties:
this.sandboxId = sandboxId; this.sandboxId = sandboxId;
this.type = type;
this.fileManager = null; this.fileManager = null;
this.terminalManager = null; this.terminalManager = null;
this.container = null; this.container = null;
@ -69,8 +70,12 @@ export class Sandbox {
console.log(`Found existing container ${this.sandboxId}`) console.log(`Found existing container ${this.sandboxId}`)
} else { } else {
console.log("Creating container", this.sandboxId) console.log("Creating container", this.sandboxId)
// Create a new container with a specified timeout // Create a new container with a specified template and timeout
this.container = await E2BSandbox.create({ const templateTypes = ["vanillajs", "reactjs", "nextjs", "streamlit"];
const template = templateTypes.includes(this.type)
? `gitwit-${this.type}`
: `base`;
this.container = await E2BSandbox.create(template, {
timeoutMs: CONTAINER_TIMEOUT, timeoutMs: CONTAINER_TIMEOUT,
}) })
} }
@ -218,9 +223,23 @@ export class Sandbox {
return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions) return this.aiWorker.generateCode(connection.userId, fileName, code, line, instructions)
} }
// Handle downloading files by download button
const handleDownloadFiles: SocketHandler = async () => {
if (!this.fileManager) throw Error("No file manager")
// Get all files with their data through fileManager
const files = this.fileManager.fileData.map((file: TFileData) => ({
path: file.id.startsWith('/') ? file.id.slice(1) : file.id,
content: file.data
}))
return { files }
}
return { return {
"heartbeat": handleHeartbeat, "heartbeat": handleHeartbeat,
"getFile": handleGetFile, "getFile": handleGetFile,
"downloadFiles": handleDownloadFiles,
"getFolder": handleGetFolder, "getFolder": handleGetFolder,
"saveFile": handleSaveFile, "saveFile": handleSaveFile,
"moveFile": handleMoveFile, "moveFile": handleMoveFile,

View File

@ -96,6 +96,7 @@ io.on("connection", async (socket) => {
userId: string userId: string
sandboxId: string sandboxId: string
isOwner: boolean isOwner: boolean
type: string
} }
// Register the connection // Register the connection
@ -111,6 +112,7 @@ io.on("connection", async (socket) => {
// Create or retrieve the sandbox manager for the given sandbox ID // Create or retrieve the sandbox manager for the given sandbox ID
const sandbox = sandboxes[data.sandboxId] ?? new Sandbox( const sandbox = sandboxes[data.sandboxId] ?? new Sandbox(
data.sandboxId, data.sandboxId,
data.type,
{ {
aiWorker, dokkuClient, gitClient, aiWorker, dokkuClient, gitClient,
} }

View File

@ -1,6 +1,6 @@
import { Socket } from "socket.io" import { Socket } from "socket.io"
import { z } from "zod" import { z } from "zod"
import { User } from "./types" import { Sandbox, User } from "./types"
// Middleware for socket authentication // Middleware for socket authentication
export const socketAuth = async (socket: Socket, next: Function) => { export const socketAuth = async (socket: Socket, next: Function) => {
@ -33,6 +33,17 @@ export const socketAuth = async (socket: Socket, next: Function) => {
) )
const dbUserJSON = (await dbUser.json()) as User const dbUserJSON = (await dbUser.json()) as User
// Fetch sandbox data from the database
const dbSandbox = await fetch(
`${process.env.DATABASE_WORKER_URL}/api/sandbox?id=${sandboxId}`,
{
headers: {
Authorization: `${process.env.WORKERS_KEY}`,
},
}
)
const dbSandboxJSON = (await dbSandbox.json()) as Sandbox
// Check if user data was retrieved successfully // Check if user data was retrieved successfully
if (!dbUserJSON) { if (!dbUserJSON) {
next(new Error("DB error.")) next(new Error("DB error."))
@ -56,6 +67,7 @@ export const socketAuth = async (socket: Socket, next: Function) => {
userId, userId,
sandboxId: sandboxId, sandboxId: sandboxId,
isOwner: sandbox !== undefined, isOwner: sandbox !== undefined,
type: dbSandboxJSON.type
} }
// Allow the connection // Allow the connection

View File

@ -12,7 +12,7 @@ export type User = {
export type Sandbox = { export type Sandbox = {
id: string id: string
name: string name: string
type: "react" | "node" type: "reactjs" | "vanillajs" | "nextjs" | "streamlit"
visibility: "public" | "private" visibility: "public" | "private"
createdAt: Date createdAt: Date
userId: string userId: string

File diff suppressed because it is too large Load Diff

View File

@ -11,13 +11,13 @@
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.1.0", "@cloudflare/vitest-pool-workers": "^0.1.0",
"@cloudflare/workers-types": "^4.20240419.0", "@cloudflare/workers-types": "^4.20241106.0",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitest": "1.3.0", "vitest": "1.3.0",
"wrangler": "^3.0.0" "wrangler": "^3.86.0"
}, },
"dependencies": { "dependencies": {
"p-limit": "^6.1.0", "p-limit": "^6.1.0",
"zod": "^3.23.4" "zod": "^3.23.4"
} }
} }

View File

@ -1,4 +1,4 @@
import pLimit from "p-limit" import { ExecutionContext, R2Bucket, Headers as CFHeaders } from "@cloudflare/workers-types"
import { z } from "zod" import { z } from "zod"
export interface Env { export interface Env {
@ -76,14 +76,13 @@ export default {
if (obj === null) { if (obj === null) {
return new Response(`${fileId} not found`, { status: 404 }) return new Response(`${fileId} not found`, { status: 404 })
} }
const headers = new Headers() const headers = new Headers() as unknown as CFHeaders
headers.set("etag", obj.httpEtag) headers.set("etag", obj.httpEtag)
obj.writeHttpMetadata(headers) obj.writeHttpMetadata(headers)
const text = await obj.text() const text = await obj.text()
return new Response(text, { return new Response(text, {
headers, headers: Object.fromEntries(headers.entries()),
}) })
} else return invalidRequest } else return invalidRequest
} else if (method === "POST") { } else if (method === "POST") {
@ -136,33 +135,7 @@ export default {
return success return success
} else if (path === "/api/init" && method === "POST") { } else if (path === "/api/init" && method === "POST") {
const initSchema = z.object({ // This API path no longer does anything, because template files are stored in E2B sandbox templates.
sandboxId: z.string(),
type: z.string(),
})
const body = await request.json()
const { sandboxId, type } = initSchema.parse(body)
console.log(`Copying template: ${type}`)
// List all objects under the directory
const { objects } = await env.Templates.list({ prefix: type })
// Copy each object to the new directory with a 5 concurrency limit
const limit = pLimit(5)
await Promise.all(
objects.map(({ key }) =>
limit(async () => {
const destinationKey = key.replace(type, `projects/${sandboxId}`)
const fileBody = await env.Templates.get(key).then(
(res) => res?.body ?? ""
)
await env.R2.put(destinationKey, fileBody)
})
)
)
return success return success
} else { } else {
return notFound return notFound

View File

@ -34,7 +34,7 @@
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": [ "types": [
"@cloudflare/workers-types/2023-07-01" "@cloudflare/workers-types"
] /* Specify type package names to be included without being referenced in a source file. */, ] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */, "resolveJsonModule": true /* Enable importing .json files */,

View File

@ -8,8 +8,10 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
# Set WORKER_URLs after deploying the workers. # Set WORKER_URLs after deploying the workers.
# Set NEXT_PUBLIC_WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml. # Set NEXT_PUBLIC_WORKERS_KEY to be the same as KEY in /backend/storage/wrangler.toml.
# These URLs should begin with https:// in production
NEXT_PUBLIC_DATABASE_WORKER_URL= NEXT_PUBLIC_DATABASE_WORKER_URL=
NEXT_PUBLIC_STORAGE_WORKER_URL= NEXT_PUBLIC_STORAGE_WORKER_URL=
NEXT_PUBLIC_AI_WORKER_URL=
NEXT_PUBLIC_WORKERS_KEY= NEXT_PUBLIC_WORKERS_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in

View File

@ -1,4 +1,4 @@
import { Room } from "@/components/editor/live/room" // import { Room } from "@/components/editor/live/room"
import Loading from "@/components/editor/loading" import Loading from "@/components/editor/loading"
import Navbar from "@/components/editor/navbar" import Navbar from "@/components/editor/navbar"
import { TerminalProvider } from "@/context/TerminalContext" import { TerminalProvider } from "@/context/TerminalContext"
@ -51,7 +51,7 @@ const getSharedUsers = async (usersToSandboxes: UsersToSandboxes[]) => {
} }
) )
const userData: User = await userRes.json() const userData: User = await userRes.json()
return { id: userData.id, name: userData.name } return { id: userData.id, name: userData.name, avatarUrl: userData.avatarUrl }
}) })
) )
@ -89,18 +89,18 @@ export default async function CodePage({ params }: { params: { id: string } }) {
return ( return (
<> <>
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background"> <div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}> {/* <Room id={sandboxId}> */}
<TerminalProvider> <TerminalProvider>
<Navbar <Navbar
userData={userData} userData={userData}
sandboxData={sandboxData} sandboxData={sandboxData}
shared={shared} shared={shared as { id: string; name: string; avatarUrl: string }[]}
/> />
<div className="w-screen flex grow"> <div className="w-screen flex grow">
<CodeEditor userData={userData} sandboxData={sandboxData} /> <CodeEditor userData={userData} sandboxData={sandboxData} />
</div> </div>
</TerminalProvider> </TerminalProvider>
</Room> {/* </Room> */}
</div> </div>
</> </>
) )

View File

@ -35,6 +35,7 @@ export default async function DashboardPage() {
type: "react" | "node" type: "react" | "node"
author: string author: string
sharedOn: Date sharedOn: Date
authorAvatarUrl: string
}[] }[]
return ( return (

View File

@ -1,6 +1,7 @@
import { User } from "@/lib/types" import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
import { redirect } from "next/navigation" import { redirect } from "next/navigation"
import { generateUniqueUsername } from "@/lib/username-generator";
export default async function AppAuthLayout({ export default async function AppAuthLayout({
children, children,
@ -24,6 +25,25 @@ export default async function AppAuthLayout({
const dbUserJSON = (await dbUser.json()) as User const dbUserJSON = (await dbUser.json()) as User
if (!dbUserJSON.id) { if (!dbUserJSON.id) {
// Try to get GitHub username if available
const githubUsername = user.externalAccounts.find(
account => account.provider === "github"
)?.username;
const username = githubUsername || await generateUniqueUsername(async (username) => {
// Check if username exists in database
const userCheck = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user/check-username?username=${username}`,
{
headers: {
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
},
}
)
const exists = await userCheck.json()
return exists.exists
});
const res = await fetch( const res = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`, `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
{ {
@ -36,9 +56,20 @@ export default async function AppAuthLayout({
id: user.id, id: user.id,
name: user.firstName + " " + user.lastName, name: user.firstName + " " + user.lastName,
email: user.emailAddresses[0].emailAddress, email: user.emailAddresses[0].emailAddress,
username: username,
avatarUrl: user.imageUrl || null,
createdAt: new Date().toISOString(),
}), }),
} }
) )
if (!res.ok) {
const error = await res.text();
console.error("Failed to create user:", error);
} else {
const data = await res.json();
console.log("User created successfully:", data);
}
} }
return <>{children}</> return <>{children}</>

View File

@ -1,57 +1,61 @@
import { colors } from "@/lib/colors" // import { colors } from "@/lib/colors"
import { User } from "@/lib/types" // import { User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
import { Liveblocks } from "@liveblocks/node" // import { Liveblocks } from "@liveblocks/node"
import { NextRequest } from "next/server" import { NextRequest } from "next/server"
const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY! // const API_KEY = process.env.LIVEBLOCKS_SECRET_KEY!
const liveblocks = new Liveblocks({ // const liveblocks = new Liveblocks({
secret: API_KEY!, // secret: API_KEY!,
}) // })
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
const clerkUser = await currentUser() // Temporarily return unauthorized while Liveblocks is disabled
return new Response("Liveblocks collaboration temporarily disabled", { status: 503 })
if (!clerkUser) { // Original implementation commented out:
return new Response("Unauthorized", { status: 401 }) // const clerkUser = await currentUser()
} //
// if (!clerkUser) {
const res = await fetch( // return new Response("Unauthorized", { status: 401 })
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${clerkUser.id}`, // }
{ //
headers: { // const res = await fetch(
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`, // `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${clerkUser.id}`,
}, // {
} // headers: {
) // Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
const user = (await res.json()) as User // },
// }
const colorNames = Object.keys(colors) // )
const randomColor = colorNames[ // const user = (await res.json()) as User
Math.floor(Math.random() * colorNames.length) //
] as keyof typeof colors // const colorNames = Object.keys(colors)
const code = colors[randomColor] // const randomColor = colorNames[
// Math.floor(Math.random() * colorNames.length)
// Create a session for the current user // ] as keyof typeof colors
// userInfo is made available in Liveblocks presence hooks, e.g. useOthers // const code = colors[randomColor]
const session = liveblocks.prepareSession(user.id, { //
userInfo: { // // Create a session for the current user
name: user.name, // // userInfo is made available in Liveblocks presence hooks, e.g. useOthers
email: user.email, // const session = liveblocks.prepareSession(user.id, {
color: randomColor, // userInfo: {
}, // name: user.name,
}) // email: user.email,
// color: randomColor,
// Give the user access to the room // },
user.sandbox.forEach((sandbox) => { // })
session.allow(`${sandbox.id}`, session.FULL_ACCESS) //
}) // // Give the user access to the room
user.usersToSandboxes.forEach((userToSandbox) => { // user.sandbox.forEach((sandbox) => {
session.allow(`${userToSandbox.sandboxId}`, session.FULL_ACCESS) // session.allow(`${sandbox.id}`, session.FULL_ACCESS)
}) // })
// user.usersToSandboxes.forEach((userToSandbox) => {
// Authorize the user and return the result // session.allow(`${userToSandbox.sandboxId}`, session.FULL_ACCESS)
const { body, status } = await session.authorize() // })
return new Response(body, { status }) //
// // Authorize the user and return the result
// const { body, status } = await session.authorize()
// return new Response(body, { status })
} }

View File

@ -18,11 +18,38 @@ export default function AboutModal({
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>About this project</DialogTitle> <DialogTitle>Help & Support</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="text-sm text-muted-foreground"> <div className="space-y-4">
Sandbox is an open-source cloud-based code editing environment with {/* <div className="text-sm text-muted-foreground">
custom AI code autocompletion and real-time collaboration. Sandbox is an open-source cloud-based code editing environment with
custom AI code autocompletion and real-time collaboration.
</div> */}
<div className="text-sm text-muted-foreground">
Get help and support through our Discord community or by creating issues on GitHub:
</div>
<div className="space-y-2">
<div className="text-sm">
<a
href="https://discord.gitwit.dev/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Join our Discord community
</a>
</div>
<div className="text-sm">
<a
href="https://github.com/jamesmurdza/sandbox/issues"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
Report issues on GitHub
</a>
</div>
</div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -25,6 +25,7 @@ export default function Dashboard({
type: "react" | "node" type: "react" | "node"
author: string author: string
sharedOn: Date sharedOn: Date
authorAvatarUrl?: string
}[] }[]
}) { }) {
const [screen, setScreen] = useState<TScreen>("projects") const [screen, setScreen] = useState<TScreen>("projects")
@ -77,14 +78,14 @@ export default function Dashboard({
<FolderDot className="w-4 h-4 mr-2" /> <FolderDot className="w-4 h-4 mr-2" />
My Projects My Projects
</Button> </Button>
<Button {/* <Button
variant="ghost" variant="ghost"
onClick={() => setScreen("shared")} onClick={() => setScreen("shared")}
className={activeScreen("shared")} className={activeScreen("shared")}
> >
<Users className="w-4 h-4 mr-2" /> <Users className="w-4 h-4 mr-2" />
Shared With Me Shared With Me
</Button> </Button> */}
{/* <Button {/* <Button
variant="ghost" variant="ghost"
onClick={() => setScreen("settings")} onClick={() => setScreen("settings")}
@ -110,7 +111,7 @@ export default function Dashboard({
className="justify-start font-normal text-muted-foreground" className="justify-start font-normal text-muted-foreground"
> >
<HelpCircle className="w-4 h-4 mr-2" /> <HelpCircle className="w-4 h-4 mr-2" />
About Help
</Button> </Button>
</div> </div>
</div> </div>
@ -121,7 +122,12 @@ export default function Dashboard({
) : null} ) : null}
</> </>
) : screen === "shared" ? ( ) : screen === "shared" ? (
<DashboardSharedWithMe shared={shared} /> <DashboardSharedWithMe
shared={shared.map((item) => ({
...item,
authorAvatarUrl: item.authorAvatarUrl || "",
}))}
/>
) : screen === "settings" ? null : null} ) : screen === "settings" ? null : null}
</div> </div>
</> </>

View File

@ -11,6 +11,7 @@ import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import Avatar from "../ui/avatar" import Avatar from "../ui/avatar"
import Button from "../ui/customButton" import Button from "../ui/customButton"
import { projectTemplates } from "@/lib/data"
export default function DashboardSharedWithMe({ export default function DashboardSharedWithMe({
shared, shared,
@ -18,8 +19,9 @@ export default function DashboardSharedWithMe({
shared: { shared: {
id: string id: string
name: string name: string
type: "react" | "node" type: string
author: string author: string
authorAvatarUrl: string
sharedOn: Date sharedOn: Date
}[] }[]
}) { }) {
@ -45,9 +47,7 @@ export default function DashboardSharedWithMe({
<Image <Image
alt="" alt=""
src={ src={
sandbox.type === "react" projectTemplates.find((p) => p.id === sandbox.type)?.icon ?? "/project-icons/node.svg"
? "/project-icons/react.svg"
: "/project-icons/node.svg"
} }
width={20} width={20}
height={20} height={20}
@ -58,7 +58,11 @@ export default function DashboardSharedWithMe({
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center"> <div className="flex items-center">
<Avatar name={sandbox.author} className="mr-2" /> <Avatar
name={sandbox.author}
avatarUrl={sandbox.authorAvatarUrl}
className="mr-2"
/>
{sandbox.author} {sandbox.author}
</div> </div>
</TableCell> </TableCell>

View File

@ -7,11 +7,11 @@ import * as monaco from "monaco-editor"
import { useCallback, useEffect, useRef, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config" // import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
import LiveblocksProvider from "@liveblocks/yjs" // import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco" // import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness" // import { Awareness } from "y-protocols/awareness"
import * as Y from "yjs" // import * as Y from "yjs"
import { import {
ResizableHandle, ResizableHandle,
@ -46,7 +46,7 @@ import { Button } from "../ui/button"
import Tab from "../ui/tab" import Tab from "../ui/tab"
import AIChat from "./AIChat" import AIChat from "./AIChat"
import GenerateInput from "./generate" import GenerateInput from "./generate"
import { Cursors } from "./live/cursors" // import { Cursors } from "./live/cursors"
import DisableAccessModal from "./live/disableModal" import DisableAccessModal from "./live/disableModal"
import Loading from "./loading" import Loading from "./loading"
import PreviewWindow from "./preview" import PreviewWindow from "./preview"
@ -147,20 +147,20 @@ export default function CodeEditor({
const isOwner = sandboxData.userId === userData.id const isOwner = sandboxData.userId === userData.id
const clerk = useClerk() const clerk = useClerk()
// Liveblocks hooks // // Liveblocks hooks
const room = useRoom() // const room = useRoom()
const [provider, setProvider] = useState<TypedLiveblocksProvider>() // const [provider, setProvider] = useState<TypedLiveblocksProvider>()
const userInfo = useSelf((me) => me.info) // const userInfo = useSelf((me) => me.info)
// Liveblocks providers map to prevent reinitializing providers // // Liveblocks providers map to prevent reinitializing providers
type ProviderData = { // type ProviderData = {
provider: LiveblocksProvider<never, never, never, never> // provider: LiveblocksProvider<never, never, never, never>
yDoc: Y.Doc // yDoc: Y.Doc
yText: Y.Text // yText: Y.Text
binding?: MonacoBinding // binding?: MonacoBinding
onSync: (isSynced: boolean) => void // onSync: (isSynced: boolean) => void
} // }
const providersMap = useRef(new Map<string, ProviderData>()) // const providersMap = useRef(new Map<string, ProviderData>())
// Refs for libraries / features // Refs for libraries / features
const editorContainerRef = useRef<HTMLDivElement>(null) const editorContainerRef = useRef<HTMLDivElement>(null)
@ -221,7 +221,6 @@ export default function CodeEditor({
let mergedConfig: any = { compilerOptions: {} } let mergedConfig: any = { compilerOptions: {} }
for (const file of tsconfigFiles) { for (const file of tsconfigFiles) {
const containerId = file.id.split("/").slice(0, 2).join("/")
const content = await fetchFileContent(file.id) const content = await fetchFileContent(file.id)
try { try {
@ -231,8 +230,7 @@ export default function CodeEditor({
if (tsConfig.references) { if (tsConfig.references) {
for (const ref of tsConfig.references) { for (const ref of tsConfig.references) {
const path = ref.path.replace("./", "") const path = ref.path.replace("./", "")
const fileId = `${containerId}/${path}` const refContent = await fetchFileContent(path)
const refContent = await fetchFileContent(fileId)
const referenceTsConfig = JSON.parse(refContent) const referenceTsConfig = JSON.parse(refContent)
// Merge configurations // Merge configurations
@ -543,8 +541,6 @@ export default function CodeEditor({
tab.id === activeFileId ? { ...tab, saved: true } : tab tab.id === activeFileId ? { ...tab, saved: true } : tab
) )
) )
console.log(`Saving file...${activeFileId}`)
console.log(`Saving file...${content}`)
socket?.emit("saveFile", { fileId: activeFileId, body: content }) socket?.emit("saveFile", { fileId: activeFileId, body: content })
} }
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
@ -575,82 +571,82 @@ export default function CodeEditor({
} }
}, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef]) }, [activeFileId, tabs, debouncedSaveData, setIsAIChatOpen, editorRef])
// Liveblocks live collaboration setup effect // // Liveblocks live collaboration setup effect
useEffect(() => { // useEffect(() => {
const tab = tabs.find((t) => t.id === activeFileId) // const tab = tabs.find((t) => t.id === activeFileId)
const model = editorRef?.getModel() // const model = editorRef?.getModel()
if (!editorRef || !tab || !model) return // if (!editorRef || !tab || !model) return
let providerData: ProviderData // let providerData: ProviderData
// When a file is opened for the first time, create a new provider and store in providersMap. // // When a file is opened for the first time, create a new provider and store in providersMap.
if (!providersMap.current.has(tab.id)) { // if (!providersMap.current.has(tab.id)) {
const yDoc = new Y.Doc() // const yDoc = new Y.Doc()
const yText = yDoc.getText(tab.id) // const yText = yDoc.getText(tab.id)
const yProvider = new LiveblocksProvider(room, yDoc) // const yProvider = new LiveblocksProvider(room, yDoc)
// Inserts the file content into the editor once when the tab is changed. // // Inserts the file content into the editor once when the tab is changed.
const onSync = (isSynced: boolean) => { // const onSync = (isSynced: boolean) => {
if (isSynced) { // if (isSynced) {
const text = yText.toString() // const text = yText.toString()
if (text === "") { // if (text === "") {
if (activeFileContent) { // if (activeFileContent) {
yText.insert(0, activeFileContent) // yText.insert(0, activeFileContent)
} else { // } else {
setTimeout(() => { // setTimeout(() => {
yText.insert(0, editorRef.getValue()) // yText.insert(0, editorRef.getValue())
}, 0) // }, 0)
} // }
} // }
} // }
} // }
yProvider.on("sync", onSync) // yProvider.on("sync", onSync)
// Save the provider to the map. // // Save the provider to the map.
providerData = { provider: yProvider, yDoc, yText, onSync } // providerData = { provider: yProvider, yDoc, yText, onSync }
providersMap.current.set(tab.id, providerData) // providersMap.current.set(tab.id, providerData)
} else { // } else {
// When a tab is opened that has been open before, reuse the existing provider. // // When a tab is opened that has been open before, reuse the existing provider.
providerData = providersMap.current.get(tab.id)! // providerData = providersMap.current.get(tab.id)!
} // }
const binding = new MonacoBinding( // const binding = new MonacoBinding(
providerData.yText, // providerData.yText,
model, // model,
new Set([editorRef]), // new Set([editorRef]),
providerData.provider.awareness as unknown as Awareness // providerData.provider.awareness as unknown as Awareness
) // )
providerData.binding = binding // providerData.binding = binding
setProvider(providerData.provider) // setProvider(providerData.provider)
return () => { // return () => {
// Cleanup logic // // Cleanup logic
if (binding) { // if (binding) {
binding.destroy() // binding.destroy()
} // }
if (providerData.binding) { // if (providerData.binding) {
providerData.binding = undefined // providerData.binding = undefined
} // }
} // }
}, [room, activeFileContent]) // }, [room, activeFileContent])
// Added this effect to clean up when the component unmounts // // Added this effect to clean up when the component unmounts
useEffect(() => { // useEffect(() => {
return () => { // return () => {
// Clean up all providers when the component unmounts // // Clean up all providers when the component unmounts
providersMap.current.forEach((data) => { // providersMap.current.forEach((data) => {
if (data.binding) { // if (data.binding) {
data.binding.destroy() // data.binding.destroy()
} // }
data.provider.disconnect() // data.provider.disconnect()
data.yDoc.destroy() // data.yDoc.destroy()
}) // })
providersMap.current.clear() // providersMap.current.clear()
} // }
}, []) // }, [])
// Connection/disconnection effect // Connection/disconnection effect
useEffect(() => { useEffect(() => {
@ -1092,9 +1088,9 @@ export default function CodeEditor({
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643 ) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
clerk.loaded ? ( clerk.loaded ? (
<> <>
{provider && userInfo ? ( {/* {provider && userInfo ? (
<Cursors yProvider={provider} userInfo={userInfo} /> <Cursors yProvider={provider} userInfo={userInfo} />
) : null} ) : null} */}
<Editor <Editor
height="100%" height="100%"
language={editorLanguage} language={editorLanguage}

View File

@ -0,0 +1,34 @@
import JSZip from 'jszip'
import { useSocket } from "@/context/SocketContext"
import { Button } from "@/components/ui/button"
import { Download } from "lucide-react"
export default function DownloadButton({ name }: { name: string }) {
const { socket } = useSocket()
const handleDownload = async () => {
socket?.emit("downloadFiles", {}, async (response: {files: {path: string, content: string}[]}) => {
const zip = new JSZip()
response.files.forEach(file => {
zip.file(file.path, file.content)
})
const blob = await zip.generateAsync({type: "blob"})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${name}.zip`
a.click()
window.URL.revokeObjectURL(url)
})
}
return (
<Button variant="outline" onClick={handleDownload}>
<Download className="w-4 h-4 mr-2" />
Download
</Button>
)
}

View File

@ -9,11 +9,12 @@ import { Pencil, Users } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
import { useState } from "react" import { useState } from "react"
import { Avatars } from "../live/avatars" // import { Avatars } from "../live/avatars"
import DeployButtonModal from "./deploy" import DeployButtonModal from "./deploy"
import EditSandboxModal from "./edit" import EditSandboxModal from "./edit"
import RunButtonModal from "./run" import RunButtonModal from "./run"
import ShareSandboxModal from "./share" import ShareSandboxModal from "./share"
import DownloadButton from "./downloadButton"
export default function Navbar({ export default function Navbar({
userData, userData,
@ -22,7 +23,7 @@ export default function Navbar({
}: { }: {
userData: User userData: User
sandboxData: Sandbox sandboxData: Sandbox
shared: { id: string; name: string }[] shared: { id: string; name: string; avatarUrl: string }[]
}) { }) {
const [isEditOpen, setIsEditOpen] = useState(false) const [isEditOpen, setIsEditOpen] = useState(false)
const [isShareOpen, setIsShareOpen] = useState(false) const [isShareOpen, setIsShareOpen] = useState(false)
@ -69,16 +70,16 @@ export default function Navbar({
sandboxData={sandboxData} sandboxData={sandboxData}
/> />
<div className="flex items-center h-full space-x-4"> <div className="flex items-center h-full space-x-4">
<Avatars /> {/* <Avatars /> */}
{isOwner ? ( {isOwner ? (
<> <>
<DeployButtonModal data={sandboxData} userData={userData} /> <DeployButtonModal data={sandboxData} userData={userData} />
<Button variant="outline" onClick={() => setIsShareOpen(true)}> {/* <Button variant="outline" onClick={() => setIsShareOpen(true)}>
<Users className="w-4 h-4 mr-2" /> <Users className="w-4 h-4 mr-2" />
Share Share
</Button> </Button> */}
</> <DownloadButton name={sandboxData.name} /></>
) : null} ) : null}
<ThemeSwitcher /> <ThemeSwitcher />
<UserButton userData={userData} /> <UserButton userData={userData} />

View File

@ -44,8 +44,8 @@ export default function RunButtonModal({
} else if (!isRunning && terminals.length < 4) { } else if (!isRunning && terminals.length < 4) {
const command = const command =
sandboxData.type === "streamlit" sandboxData.type === "streamlit"
? "pip install -r requirements.txt && streamlit run main.py --server.runOnSave true" ? "./venv/bin/streamlit run main.py --server.runOnSave true"
: "yarn install && yarn dev" : "npm run dev"
try { try {
// Create a new terminal with the appropriate command // Create a new terminal with the appropriate command

View File

@ -43,6 +43,7 @@ export default function ShareSandboxModal({
shared: { shared: {
id: string id: string
name: string name: string
avatarUrl: string
}[] }[]
}) { }) {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -142,7 +143,11 @@ export default function ShareSandboxModal({
</DialogHeader> </DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
{shared.map((user) => ( {shared.map((user) => (
<SharedUser key={user.id} user={user} sandboxId={data.id} /> <SharedUser
key={user.id}
user={user}
sandboxId={data.id}
/>
))} ))}
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ export default function SharedUser({
user, user,
sandboxId, sandboxId,
}: { }: {
user: { id: string; name: string } user: { id: string; name: string; avatarUrl: string }
sandboxId: string sandboxId: string
}) { }) {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@ -24,7 +24,7 @@ export default function SharedUser({
return ( return (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center"> <div className="flex items-center">
<Avatar name={user.name} className="mr-2" /> <Avatar name={user.name} avatarUrl={user.avatarUrl} className="mr-2" />
{user.name} {user.name}
</div> </div>
<Button <Button

View File

@ -45,9 +45,14 @@ export default function Landing() {
<h1 className="text-2xl font-medium text-center mt-16"> <h1 className="text-2xl font-medium text-center mt-16">
A Collaborative + AI-Powered Code Environment A Collaborative + AI-Powered Code Environment
</h1> </h1>
<p className="text-muted-foreground mt-4 text-center "> {/* <p className="text-muted-foreground mt-4 text-center ">
Sandbox is an open-source cloud-based code editing environment with Sandbox is an open-source cloud-based code editing environment with
custom AI code autocompletion and real-time collaboration. custom AI code autocompletion and real-time collaboration.
</p> */}
<p className="text-muted-foreground mt-4 text-center ">
A cloud-based code editor featuring real-time collaboration,
intelligent code autocompletion, and an AI assistant to help you code
faster and smarter.
</p> </p>
<div className="mt-8 flex space-x-4"> <div className="mt-8 flex space-x-4">
<Link href="/sign-up"> <Link href="/sign-up">

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -1,23 +1,42 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import Image from "next/image"
export default function Avatar({ export default function Avatar({
name, name,
avatarUrl,
className, className,
}: { }: {
name: string name: string
avatarUrl?: string | null
className?: string className?: string
}) { }) {
// Generate initials from name if no avatarUrl is provided
const initials = name
? name
.split(" ")
.slice(0, 2)
.map((letter) => letter[0].toUpperCase())
.join("")
: "?"
return ( return (
<div <div
className={cn( className={cn(
className, className,
"w-5 h-5 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-[0.5rem] font-medium" "w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium"
)} )}
> >
{name {avatarUrl ? (
.split(" ") <Image
.slice(0, 2) src={avatarUrl}
.map((letter) => letter[0].toUpperCase())} alt={name || "User"}
width={20}
height={20}
className="w-full h-full object-cover"
/>
) : (
initials
)}
</div> </div>
) )
} }

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -11,6 +11,7 @@ import { User } from "@/lib/types"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
import { LogOut, Sparkles } from "lucide-react" import { LogOut, Sparkles } from "lucide-react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import Avatar from "./avatar"
export default function UserButton({ userData }: { userData: User }) { export default function UserButton({ userData }: { userData: User }) {
if (!userData) return null if (!userData) return null
@ -21,13 +22,7 @@ export default function UserButton({ userData }: { userData: User }) {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<div className="w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium"> <Avatar name={userData.name} avatarUrl={userData.avatarUrl} />
{userData.name &&
userData.name
.split(" ")
.slice(0, 2)
.map((name) => name[0].toUpperCase())}
</div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-48" align="end"> <DropdownMenuContent className="w-48" align="end">
<div className="py-1.5 px-2 w-full"> <div className="py-1.5 px-2 w-full">

View File

@ -16,7 +16,7 @@ export const projectTemplates: {
id: "vanillajs", id: "vanillajs",
name: "HTML/JS", name: "HTML/JS",
icon: "/project-icons/more.svg", icon: "/project-icons/more.svg",
description: "More coming soon, feel free to contribute on GitHub", description: "A simple HTML/JS project for building web apps",
disabled: false, disabled: false,
}, },
{ {

View File

@ -4,6 +4,9 @@ export type User = {
id: string id: string
name: string name: string
email: string email: string
username: string
avatarUrl: string | null
createdAt: Date
generations: number generations: number
sandbox: Sandbox[] sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[] usersToSandboxes: UsersToSandboxes[]

View File

@ -0,0 +1,82 @@
// Constants for username generation
const WORDS = {
adjectives: [
"azure", "crimson", "golden", "silver", "violet", "emerald", "cobalt", "amber", "coral", "jade",
"cyber", "digital", "quantum", "neural", "binary", "cosmic", "stellar", "atomic", "crypto", "nano",
"swift", "brave", "clever", "wise", "noble", "rapid", "bright", "sharp", "keen", "bold",
"dynamic", "epic", "mega", "ultra", "hyper", "super", "prime", "elite", "alpha", "omega",
"pixel", "vector", "sonic", "laser", "matrix", "nexus", "proxy", "cloud", "data", "tech",
],
nouns: [
"coder", "hacker", "dev", "ninja", "guru", "wizard", "admin", "mod", "chief", "boss",
"wolf", "eagle", "phoenix", "dragon", "tiger", "falcon", "shark", "lion", "hawk", "bear",
"byte", "bit", "node", "stack", "cache", "chip", "core", "net", "web", "app",
"star", "nova", "pulsar", "comet", "nebula", "quasar", "cosmos", "orbit", "astro", "solar",
"mind", "soul", "spark", "pulse", "force", "power", "wave", "storm", "flash", "surge",
],
prefixes: [
"the", "mr", "ms", "dr", "pro", "master", "lord", "captain", "chief", "agent",
],
} as const;
// Helper function to get random element from array
const getRandomElement = <T>(array: readonly T[]): T => {
return array[Math.floor(Math.random() * array.length)];
};
// Username pattern generators
const usernamePatterns = {
basic: (): string => {
const adjective = getRandomElement(WORDS.adjectives);
const noun = getRandomElement(WORDS.nouns);
const number = Math.floor(Math.random() * 10000);
return `${adjective}${noun}${number}`;
},
prefixed: (): string => {
const prefix = getRandomElement(WORDS.prefixes);
const noun = getRandomElement(WORDS.nouns);
const number = Math.floor(Math.random() * 100);
return `${prefix}${noun}${number}`;
},
doubleAdjective: (): string => {
const adj1 = getRandomElement(WORDS.adjectives);
const adj2 = getRandomElement(WORDS.adjectives);
const noun = getRandomElement(WORDS.nouns);
return `${adj1}${adj2}${noun}`;
},
doubleNoun: (): string => {
const noun1 = getRandomElement(WORDS.nouns);
const noun2 = getRandomElement(WORDS.nouns);
const number = Math.floor(Math.random() * 100);
return `${noun1}${number}${noun2}`;
},
};
export function generateUsername(): string {
const patterns = Object.values(usernamePatterns);
const selectedPattern = getRandomElement(patterns);
return selectedPattern();
}
export async function generateUniqueUsername(
checkExists: (username: string) => Promise<boolean>
): Promise<string> {
const MAX_ATTEMPTS = 10;
let attempts = 0;
let username = generateUsername();
while (await checkExists(username) && attempts < MAX_ATTEMPTS) {
username = generateUsername();
attempts++;
}
if (attempts >= MAX_ATTEMPTS) {
// Add a large random number to ensure uniqueness
username = generateUsername() + Math.floor(Math.random() * 1000000);
}
return username;
}

View File

@ -5,6 +5,12 @@ const nextConfig = {
{ {
hostname: "cdn.simpleicons.org", hostname: "cdn.simpleicons.org",
}, },
{
hostname: "img.clerk.com",
},
{
hostname: "images.clerk.dev",
},
], ],
}, },
} }

File diff suppressed because it is too large Load Diff

View File

@ -27,9 +27,11 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.1.3",
"@react-three/fiber": "^8.16.6", "@react-three/fiber": "^8.16.6",
"@uiw/codemirror-theme-vscode": "^4.23.5", "@uiw/codemirror-theme-vscode": "^4.23.5",
"@uiw/react-codemirror": "^4.23.5", "@uiw/react-codemirror": "^4.23.5",
@ -44,6 +46,7 @@
"framer-motion": "^11.2.3", "framer-motion": "^11.2.3",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"geist": "^1.3.0", "geist": "^1.3.0",
"jszip": "^3.10.1",
"lucide-react": "^0.365.0", "lucide-react": "^0.365.0",
"monaco-themes": "^0.4.4", "monaco-themes": "^0.4.4",
"next": "14.1.3", "next": "14.1.3",
@ -56,6 +59,7 @@
"react-resizable-panels": "^2.0.16", "react-resizable-panels": "^2.0.16",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"shadcn": "^2.1.6",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",
"sonner": "^1.4.41", "sonner": "^1.4.41",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",

2299
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
{
"dependencies": {
"@radix-ui/react-popover": "^1.1.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.15"
}
}