Compare commits
28 Commits
ai-codegen
...
new-schema
Author | SHA1 | Date | |
---|---|---|---|
0828209455 | |||
94ca5b2c9f | |||
2a58d0a5e3 | |||
30c9da559f | |||
2262adca74 | |||
b486d22111 | |||
81399cd351 | |||
96812027d0 | |||
85abbbdb0b | |||
3db3fbc490 | |||
c6c01101f1 | |||
9c6067dcd9 | |||
9c98e41ebb | |||
c669babb2f | |||
474102aa14 | |||
5a63ab7265 | |||
5633727bdb | |||
39911e9ef2 | |||
f35330ba4f | |||
9197050ca3 | |||
ebb270911b | |||
60c5345753 | |||
95154af074 | |||
7ed2d14435 | |||
198b59aa55 | |||
f6077ed516 | |||
684ee20a03 | |||
e658a84a9b |
46
README.md
46
README.md
@ -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
|
||||||
|
|
||||||
|
@ -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": {},
|
||||||
|
@ -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": {},
|
||||||
|
@ -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": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
9950
backend/database/package-lock.json
generated
9950
backend/database/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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=
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
6221
backend/storage/package-lock.json
generated
6221
backend/storage/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 */,
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -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 (
|
||||||
|
@ -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}</>
|
||||||
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
34
frontend/components/editor/navbar/downloadButton.tsx
Normal file
34
frontend/components/editor/navbar/downloadButton.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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} />
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
59
frontend/components/ui/alert.tsx
Normal file
59
frontend/components/ui/alert.tsx
Normal 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 }
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
28
frontend/components/ui/progress.tsx
Normal file
28
frontend/components/ui/progress.tsx
Normal 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 }
|
32
frontend/components/ui/tooltip.tsx
Normal file
32
frontend/components/ui/tooltip.tsx
Normal 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 }
|
@ -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">
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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[]
|
||||||
|
82
frontend/lib/username-generator.ts
Normal file
82
frontend/lib/username-generator.ts
Normal 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;
|
||||||
|
}
|
@ -5,6 +5,12 @@ const nextConfig = {
|
|||||||
{
|
{
|
||||||
hostname: "cdn.simpleicons.org",
|
hostname: "cdn.simpleicons.org",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
hostname: "img.clerk.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hostname: "images.clerk.dev",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
2069
frontend/package-lock.json
generated
2069
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
2299
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/react-popover": "^1.1.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tailwindcss/typography": "^0.5.15"
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user