Compare commits

..

3 Commits

Author SHA1 Message Date
Akhileshrangani4
46c715b1eb feat: integrate template awareness into AI assistant
- Add template configurations with file structures and conventions
- Update AI route handler to include template context in system messages
- Pass template type through AIChat component
- Add template-specific run commands
- Enhance AI responses with project structure knowledge
- Move hardcoded run commands from navbar/run.tsx to templates/index.ts

This improves the AI's understanding of different project templates (React, Next.js, Streamlit, Vanilla JS) and enables more contextual assistance based on the project type.
2024-11-24 00:22:10 -05:00
Akhileshrangani4
d87f818241 fix: scroll-up while ai is generating content
- added a scroll-to-bottom button
- users can now scroll-up while generating content
2024-11-23 21:55:44 -05:00
Akhileshrangani4
a25097108d refactor(api): remove AI worker, add ai api route, add usage tiers
- Remove separate AI worker service
- Added generation limits:
  FREE: 1000/month (For the beta version)
  PRO: 500/month
  ENTERPRISE: 1000/month
- Integrate AI functionality into main API routes
- Added monthly generations reset and usage tier upgrade API routes
- Upgrade tier page to be added along with profile page section
2024-11-23 20:31:24 -05:00
77 changed files with 1810 additions and 7851 deletions

1
.gitignore vendored
View File

@ -40,6 +40,7 @@ wrangler.toml
dist dist
backend/server/projects backend/server/projects
backend/database/drizzle
app.yaml app.yaml
ingressController.yaml ingressController.yaml

View File

@ -5,9 +5,5 @@
"source.fixAll": "explicit", "source.fixAll": "explicit",
"source.organizeImports": "explicit" "source.organizeImports": "explicit"
}, },
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
],
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
} }

View File

@ -2,7 +2,7 @@
![2024-10-2307 17 42-ezgif com-resize](https://github.com/user-attachments/assets/a4057129-81a7-4a31-a093-c8bc8189ae72) ![2024-10-2307 17 42-ezgif com-resize](https://github.com/user-attachments/assets/a4057129-81a7-4a31-a093-c8bc8189ae72)
Sandbox is an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat. Sandbox is an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration and AI chat.
For the latest updates, join our Discord server: [discord.gitwit.dev](https://discord.gitwit.dev/). For the latest updates, join our Discord server: [discord.gitwit.dev](https://discord.gitwit.dev/).
@ -10,11 +10,11 @@ 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.
### 0. Requirements ### 0. Requirements
The application uses NodeJS for the backend, NextJS for the frontend, and Cloudflare workers for additional backend tasks. The application uses NodeJS for the backend, NextJS for the frontend and Cloudflare workers for additional backend tasks.
Needed accounts to set up: Needed accounts to set up:
@ -22,7 +22,6 @@ Needed accounts to set up:
- [Liveblocks](https://liveblocks.io/): Used for collaborative editing. - [Liveblocks](https://liveblocks.io/): Used for collaborative editing.
- [E2B](https://e2b.dev/): Used for the terminals and live preview. - [E2B](https://e2b.dev/): Used for the terminals and live preview.
- [Cloudflare](https://www.cloudflare.com/): Used for relational data storage (D2) and file storage (R2). - [Cloudflare](https://www.cloudflare.com/): Used for relational data storage (D2) and file storage (R2).
- [Anthropic](https://anthropic.com/) and [OpenAI](https://openai.com/): API keys for code generation.
A quick overview of the tech before we start: The deployment uses a **NextJS** app for the frontend and an **ExpressJS** server on the backend. Presumably that's because NextJS integrates well with Clerk middleware but not with Socket.io. A quick overview of the tech before we start: The deployment uses a **NextJS** app for the frontend and an **ExpressJS** server on the backend. Presumably that's because NextJS integrates well with Clerk middleware but not with Socket.io.
@ -42,6 +41,7 @@ Run `npm install` in:
/backend/database /backend/database
/backend/storage /backend/storage
/backend/server /backend/server
/backend/ai
``` ```
### 2. Adding Clerk ### 2. Adding Clerk
@ -78,8 +78,6 @@ npx wrangler deploy
### 4. Deploying the database ### 4. Deploying the database
Follow this [guide](https://docs.google.com/document/d/1w5dA5daic_sIYB5Seni1KvnFx51pPV2so6lLdN2xa7Q/edit?usp=sharing) for more info.
Create a database: Create a database:
``` ```
@ -145,7 +143,21 @@ Update `/backend/server/.env`:
E2B_API_KEY='🔑' E2B_API_KEY='🔑'
``` ```
### 9. Configuring the frontend ### 9. Adding AI code generation
In the `/backend/ai` directory:
```
npx wrangler deploy
```
Update `/backend/server/.env`:
```
AI_WORKER_URL='https://ai.🍎.workers.dev'
```
### 10. Configuring the frontend
Update `/frontend/.env`: Update `/frontend/.env`:
@ -153,11 +165,9 @@ Update `/frontend/.env`:
NEXT_PUBLIC_DATABASE_WORKER_URL='https://database.🍎.workers.dev' NEXT_PUBLIC_DATABASE_WORKER_URL='https://database.🍎.workers.dev'
NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev' NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET' NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET'
ANTHROPIC_API_KEY='🔑'
OPENAI_API_KEY='🔑'
``` ```
### 10. Running the IDE ### 11. Running the IDE
Run `npm run dev` simultaneously in: Run `npm run dev` simultaneously in:
@ -176,15 +186,6 @@ Setting up deployments first requires a separate domain (such as gitwit.app, whi
We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d
And we install [dokku-daemon](https://github.com/dokku/dokku-daemon) with the following commands:
```
git clone https://github.com/dokku/dokku-daemon
cd dokku-daemon
sudo make install
systemctl start dokku-daemon
```
The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env: The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env:
```bash ```bash
@ -193,21 +194,16 @@ DOKKU_USERNAME=
DOKKU_KEY= DOKKU_KEY=
``` ```
## Deploying to AWS
The backend server and deployments server can be deployed using AWS's EC2 service. See [our video guide](https://www.youtube.com/watch?v=WN8HQnimjmk) on how to do this.
## Creating Custom Templates ## Creating Custom Templates
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. 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.
Currently there are five templates: Currently there are four templates:
- [jamesmurdza/dokku-reactjs-template](https://github.com/jamesmurdza/dokku-reactjs-template) - [jamesmurdza/dokku-reactjs-template](https://github.com/jamesmurdza/dokku-reactjs-template)
- [jamesmurdza/dokku-vanillajs-template](https://github.com/jamesmurdza/dokku-vanillajs-template) - [jamesmurdza/dokku-vanillajs-template](https://github.com/jamesmurdza/dokku-vanillajs-template)
- [jamesmurdza/dokku-nextjs-template](https://github.com/jamesmurdza/dokku-nextjs-template) - [jamesmurdza/dokku-nextjs-template](https://github.com/jamesmurdza/dokku-nextjs-template)
- [jamesmurdza/dokku-streamlit-template](https://github.com/jamesmurdza/dokku-streamlit-template) - [jamesmurdza/dokku-streamlit-template](https://github.com/jamesmurdza/dokku-streamlit-template)
- [omarrwd/dokku-php-template](https://github.com/omarrwd/dokku-php-template)
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 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.
@ -262,7 +258,8 @@ backend/
├── database/ ├── database/
│ ├── src │ ├── src
│ └── drizzle │ └── drizzle
└── storage ├── storage
└── ai
``` ```
| Path | Description | | Path | Description |
@ -271,6 +268,7 @@ backend/
| `backend/server` | The Express websocket server. | | `backend/server` | The Express websocket server. |
| `backend/database` | API for interfacing with the D1 database (SQLite). | | `backend/database` | API for interfacing with the D1 database (SQLite). |
| `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. | | `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. |
| `backend/ai` | API for making requests to Workers AI . |
### Development ### Development

View File

@ -1,46 +0,0 @@
CREATE TABLE `sandbox` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`type` text NOT NULL,
`visibility` text,
`createdAt` integer DEFAULT CURRENT_TIMESTAMP,
`user_id` text NOT NULL,
`likeCount` integer DEFAULT 0,
`viewCount` integer DEFAULT 0,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `sandbox_likes` (
`user_id` text NOT NULL,
`sandbox_id` text NOT NULL,
`createdAt` integer DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(`sandbox_id`, `user_id`),
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`sandbox_id`) REFERENCES `sandbox`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL,
`username` text NOT NULL,
`avatarUrl` text,
`githubToken` text,
`createdAt` integer DEFAULT CURRENT_TIMESTAMP,
`generations` integer DEFAULT 0,
`tier` text DEFAULT 'FREE',
`tierExpiresAt` integer,
`lastResetDate` integer
);
--> statement-breakpoint
CREATE TABLE `users_to_sandboxes` (
`userId` text NOT NULL,
`sandboxId` text NOT NULL,
`sharedOn` integer,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`sandboxId`) REFERENCES `sandbox`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `sandbox_id_unique` ON `sandbox` (`id`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_id_unique` ON `user` (`id`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);

View File

@ -1,3 +0,0 @@
ALTER TABLE user ADD `bio` text;--> statement-breakpoint
ALTER TABLE user ADD `personalWebsite` text;--> statement-breakpoint
ALTER TABLE user ADD `links` text DEFAULT '[]';

View File

@ -1 +0,0 @@
ALTER TABLE sandbox ADD `containerId` text;

View File

@ -1,7 +1,7 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "4ada398d-7e4e-448f-8cea-a10b4d844600", "id": "3241d14f-687f-4134-94ab-88bf36e8611e",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"sandbox": { "sandbox": {
@ -94,72 +94,6 @@
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"sandbox_likes": {
"name": "sandbox_likes",
"columns": {
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sandbox_id": {
"name": "sandbox_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
}
},
"indexes": {},
"foreignKeys": {
"sandbox_likes_user_id_user_id_fk": {
"name": "sandbox_likes_user_id_user_id_fk",
"tableFrom": "sandbox_likes",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"sandbox_likes_sandbox_id_sandbox_id_fk": {
"name": "sandbox_likes_sandbox_id_sandbox_id_fk",
"tableFrom": "sandbox_likes",
"tableTo": "sandbox",
"columnsFrom": [
"sandbox_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"sandbox_likes_sandbox_id_user_id_pk": {
"columns": [
"sandbox_id",
"user_id"
],
"name": "sandbox_likes_sandbox_id_user_id_pk"
}
},
"uniqueConstraints": {}
},
"user": { "user": {
"name": "user", "name": "user",
"columns": { "columns": {
@ -198,13 +132,6 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"githubToken": {
"name": "githubToken",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": { "createdAt": {
"name": "createdAt", "name": "createdAt",
"type": "integer", "type": "integer",
@ -220,28 +147,6 @@
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": 0 "default": 0
},
"tier": {
"name": "tier",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'FREE'"
},
"tierExpiresAt": {
"name": "tierExpiresAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"lastResetDate": {
"name": "lastResetDate",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
} }
}, },
"indexes": { "indexes": {

View File

@ -1,353 +1,258 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "80c0b0b2-bb0e-449a-b447-c21863686f58", "id": "7eafd85c-d63d-43be-aa26-1a0eb2ca1805",
"prevId": "4ada398d-7e4e-448f-8cea-a10b4d844600", "prevId": "3241d14f-687f-4134-94ab-88bf36e8611e",
"tables": { "tables": {
"sandbox": { "sandbox": {
"name": "sandbox", "name": "sandbox",
"columns": { "columns": {
"id": { "id": {
"name": "id", "name": "id",
"type": "text", "type": "text",
"primaryKey": true, "primaryKey": true,
"notNull": true, "notNull": true,
"autoincrement": false "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
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"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": { "name": {
"sandbox_id_unique": { "name": "name",
"name": "sandbox_id_unique", "type": "text",
"columns": [ "primaryKey": false,
"id" "notNull": true,
], "autoincrement": false
"isUnique": true
}
}, },
"foreignKeys": { "type": {
"sandbox_user_id_user_id_fk": { "name": "type",
"name": "sandbox_user_id_user_id_fk", "type": "text",
"tableFrom": "sandbox", "primaryKey": false,
"tableTo": "user", "notNull": true,
"columnsFrom": [ "autoincrement": false
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
}, },
"compositePrimaryKeys": {}, "visibility": {
"uniqueConstraints": {} "name": "visibility",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"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
}
}, },
"sandbox_likes": { "indexes": {
"name": "sandbox_likes", "sandbox_id_unique": {
"columns": { "name": "sandbox_id_unique",
"user_id": { "columns": [
"name": "user_id", "id"
"type": "text", ],
"primaryKey": false, "isUnique": true
"notNull": true, }
"autoincrement": false
},
"sandbox_id": {
"name": "sandbox_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
}
},
"indexes": {},
"foreignKeys": {
"sandbox_likes_user_id_user_id_fk": {
"name": "sandbox_likes_user_id_user_id_fk",
"tableFrom": "sandbox_likes",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"sandbox_likes_sandbox_id_sandbox_id_fk": {
"name": "sandbox_likes_sandbox_id_sandbox_id_fk",
"tableFrom": "sandbox_likes",
"tableTo": "sandbox",
"columnsFrom": [
"sandbox_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"sandbox_likes_sandbox_id_user_id_pk": {
"columns": [
"sandbox_id",
"user_id"
],
"name": "sandbox_likes_sandbox_id_user_id_pk"
}
},
"uniqueConstraints": {}
}, },
"user": { "foreignKeys": {
"name": "user", "sandbox_user_id_user_id_fk": {
"columns": { "name": "sandbox_user_id_user_id_fk",
"id": { "tableFrom": "sandbox",
"name": "id", "tableTo": "user",
"type": "text", "columnsFrom": [
"primaryKey": true, "user_id"
"notNull": true, ],
"autoincrement": false "columnsTo": [
}, "id"
"name": { ],
"name": "name", "onDelete": "no action",
"type": "text", "onUpdate": "no action"
"primaryKey": false, }
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatarUrl": {
"name": "avatarUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"githubToken": {
"name": "githubToken",
"type": "text",
"primaryKey": false,
"notNull": 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
},
"bio": {
"name": "bio",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"personalWebsite": {
"name": "personalWebsite",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"links": {
"name": "links",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'[]'"
},
"tier": {
"name": "tier",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'FREE'"
},
"tierExpiresAt": {
"name": "tierExpiresAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"lastResetDate": {
"name": "lastResetDate",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_id_unique": {
"name": "user_id_unique",
"columns": [
"id"
],
"isUnique": true
},
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}, },
"users_to_sandboxes": { "compositePrimaryKeys": {},
"name": "users_to_sandboxes", "uniqueConstraints": {}
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sandboxId": {
"name": "sandboxId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sharedOn": {
"name": "sharedOn",
"type": "integer",
"primaryKey": false,
"notNull": false,
"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": {}, "user": {
"_meta": { "name": "user",
"schemas": {}, "columns": {
"tables": {}, "id": {
"columns": {} "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
},
"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"
},
"generations": {
"name": "generations",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 0
},
"tier": {
"name": "tier",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'FREE'"
},
"tierExpiresAt": {
"name": "tierExpiresAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"lastResetDate": {
"name": "lastResetDate",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_id_unique": {
"name": "user_id_unique",
"columns": [
"id"
],
"isUnique": true
},
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"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
},
"sharedOn": {
"name": "sharedOn",
"type": "integer",
"primaryKey": false,
"notNull": false,
"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,360 +0,0 @@
{
"version": "5",
"dialect": "sqlite",
"id": "51abcf01-2921-4885-8058-d1ccd576f3e1",
"prevId": "80c0b0b2-bb0e-449a-b447-c21863686f58",
"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
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"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
},
"containerId": {
"name": "containerId",
"type": "text",
"primaryKey": false,
"notNull": false,
"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": {}
},
"sandbox_likes": {
"name": "sandbox_likes",
"columns": {
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sandbox_id": {
"name": "sandbox_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
}
},
"indexes": {},
"foreignKeys": {
"sandbox_likes_user_id_user_id_fk": {
"name": "sandbox_likes_user_id_user_id_fk",
"tableFrom": "sandbox_likes",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"sandbox_likes_sandbox_id_sandbox_id_fk": {
"name": "sandbox_likes_sandbox_id_sandbox_id_fk",
"tableFrom": "sandbox_likes",
"tableTo": "sandbox",
"columnsFrom": [
"sandbox_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"sandbox_likes_sandbox_id_user_id_pk": {
"columns": [
"sandbox_id",
"user_id"
],
"name": "sandbox_likes_sandbox_id_user_id_pk"
}
},
"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
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatarUrl": {
"name": "avatarUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"githubToken": {
"name": "githubToken",
"type": "text",
"primaryKey": false,
"notNull": 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
},
"bio": {
"name": "bio",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"personalWebsite": {
"name": "personalWebsite",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"links": {
"name": "links",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'[]'"
},
"tier": {
"name": "tier",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'FREE'"
},
"tierExpiresAt": {
"name": "tierExpiresAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"lastResetDate": {
"name": "lastResetDate",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_id_unique": {
"name": "user_id_unique",
"columns": [
"id"
],
"isUnique": true
},
"user_username_unique": {
"name": "user_username_unique",
"columns": [
"username"
],
"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
},
"sharedOn": {
"name": "sharedOn",
"type": "integer",
"primaryKey": false,
"notNull": false,
"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,22 +5,15 @@
{ {
"idx": 0, "idx": 0,
"version": "5", "version": "5",
"when": 1736155854410, "when": 1732400315508,
"tag": "0000_sudden_wallop", "tag": "0000_milky_hardball",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "5", "version": "5",
"when": 1736169498666, "when": 1732408530094,
"tag": "0001_dusty_komodo", "tag": "0001_freezing_supreme_intelligence",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1736768910615,
"tag": "0002_chemical_brother_voodoo",
"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.101.0" "wrangler": "^3.86.0"
}, },
"dependencies": { "dependencies": {
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",

View File

@ -5,599 +5,379 @@ import { z } from "zod"
import { and, eq, sql } from "drizzle-orm" import { and, eq, sql } from "drizzle-orm"
import * as schema from "./schema" import * as schema from "./schema"
import { Sandbox, sandbox, sandboxLikes, user, usersToSandboxes } from "./schema" import { sandbox, user, usersToSandboxes } from "./schema"
export interface Env { export interface Env {
DB: D1Database DB: D1Database
STORAGE: any STORAGE: any
KEY: string KEY: string
STORAGE_WORKER_URL: string STORAGE_WORKER_URL: string
} }
// https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1 // https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1
// npm run generate // npm run generate
// npx wrangler d1 execute d1-sandbox --local --file=./drizzle/<FILE> // npx wrangler d1 execute d1-sandbox --local --file=./drizzle/<FILE>
interface SandboxWithLiked extends Sandbox {
liked: boolean
}
interface UserResponse extends Omit<schema.User, "sandbox"> {
sandbox: SandboxWithLiked[]
}
export default { export default {
async fetch( async fetch(
request: Request, request: Request,
env: Env, env: Env,
ctx: ExecutionContext ctx: ExecutionContext
): Promise<Response> { ): Promise<Response> {
const success = new Response("Success", { status: 200 }) const success = new Response("Success", { status: 200 })
const invalidRequest = new Response("Invalid Request", { status: 400 }) const invalidRequest = new Response("Invalid Request", { status: 400 })
const notFound = new Response("Not Found", { status: 404 }) const notFound = new Response("Not Found", { status: 404 })
const methodNotAllowed = new Response("Method Not Allowed", { status: 405 }) const methodNotAllowed = new Response("Method Not Allowed", { status: 405 })
const url = new URL(request.url) const url = new URL(request.url)
const path = url.pathname const path = url.pathname
const method = request.method const method = request.method
if (request.headers.get("Authorization") !== env.KEY) { if (request.headers.get("Authorization") !== env.KEY) {
return new Response("Unauthorized", { status: 401 }) return new Response("Unauthorized", { status: 401 })
} }
const db = drizzle(env.DB, { schema }) const db = drizzle(env.DB, { schema })
if (path === "/api/sandbox") { if (path === "/api/sandbox") {
if (method === "GET") { if (method === "GET") {
const params = url.searchParams const params = url.searchParams
if (params.has("id")) { if (params.has("id")) {
const id = params.get("id") as string const id = params.get("id") as string
const res = await db.query.sandbox.findFirst({ const res = await db.query.sandbox.findFirst({
where: (sandbox, { eq }) => eq(sandbox.id, id), where: (sandbox, { eq }) => eq(sandbox.id, id),
with: { with: {
usersToSandboxes: true, usersToSandboxes: true,
}, },
}) })
return json(res ?? {}) return json(res ?? {})
} else { } else {
const res = await db.select().from(sandbox).all() const res = await db.select().from(sandbox).all()
return json(res ?? {}) return json(res ?? {})
} }
} else if (method === "DELETE") { } else if (method === "DELETE") {
const params = url.searchParams const params = url.searchParams
if (params.has("id")) { if (params.has("id")) {
const id = params.get("id") as string const id = params.get("id") as string
await db.delete(sandboxLikes).where(eq(sandboxLikes.sandboxId, id)) await db
await db .delete(usersToSandboxes)
.delete(usersToSandboxes) .where(eq(usersToSandboxes.sandboxId, id))
.where(eq(usersToSandboxes.sandboxId, id)) await db.delete(sandbox).where(eq(sandbox.id, id))
await db.delete(sandbox).where(eq(sandbox.id, id))
const deleteStorageRequest = new Request( const deleteStorageRequest = new Request(
`${env.STORAGE_WORKER_URL}/api/project`, `${env.STORAGE_WORKER_URL}/api/project`,
{ {
method: "DELETE", method: "DELETE",
body: JSON.stringify({ sandboxId: id }), body: JSON.stringify({ sandboxId: id }),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `${env.KEY}`, Authorization: `${env.KEY}`,
}, },
} }
) )
await env.STORAGE.fetch(deleteStorageRequest) await env.STORAGE.fetch(deleteStorageRequest)
return success return success
} else { } else {
return invalidRequest return invalidRequest
} }
} else if (method === "POST") { } else if (method === "POST") {
const postSchema = z.object({ const postSchema = z.object({
id: z.string(), id: z.string(),
name: z.string().optional(), name: z.string().optional(),
visibility: z.enum(["public", "private"]).optional(), visibility: z.enum(["public", "private"]).optional(),
}) })
const body = await request.json() const body = await request.json()
const { id, name, visibility } = postSchema.parse(body) const { id, name, visibility } = postSchema.parse(body)
const sb = await db const sb = await db
.update(sandbox) .update(sandbox)
.set({ name, visibility }) .set({ name, visibility })
.where(eq(sandbox.id, id)) .where(eq(sandbox.id, id))
.returning() .returning()
.get() .get()
return success return success
} else if (method === "PUT") { } else if (method === "PUT") {
const initSchema = z.object({ const initSchema = z.object({
type: z.string(), type: z.string(),
name: z.string(), name: z.string(),
userId: z.string(), userId: z.string(),
visibility: z.enum(["public", "private"]), visibility: z.enum(["public", "private"]),
}) })
const body = await request.json() const body = await request.json()
const { type, name, userId, visibility } = initSchema.parse(body) const { type, name, userId, visibility } = initSchema.parse(body)
const userSandboxes = await db const userSandboxes = await db
.select() .select()
.from(sandbox) .from(sandbox)
.where(eq(sandbox.userId, userId)) .where(eq(sandbox.userId, userId))
.all() .all()
if (userSandboxes.length >= 8) { if (userSandboxes.length >= 8) {
return new Response("You reached the maximum # of sandboxes.", { return new Response("You reached the maximum # of sandboxes.", {
status: 400, status: 400,
}) })
} }
const sb = await db const sb = await db
.insert(sandbox) .insert(sandbox)
.values({ type, name, userId, visibility, createdAt: new Date() }) .values({ type, name, userId, visibility, createdAt: new Date() })
.returning() .returning()
.get() .get()
const initStorageRequest = new Request( const initStorageRequest = new Request(
`${env.STORAGE_WORKER_URL}/api/init`, `${env.STORAGE_WORKER_URL}/api/init`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ sandboxId: sb.id, type }), body: JSON.stringify({ sandboxId: sb.id, type }),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `${env.KEY}`, Authorization: `${env.KEY}`,
}, },
} }
) )
await env.STORAGE.fetch(initStorageRequest) await env.STORAGE.fetch(initStorageRequest)
return new Response(sb.id, { status: 200 }) return new Response(sb.id, { status: 200 })
} else { } else {
return methodNotAllowed return methodNotAllowed
} }
} else if (path === "/api/sandbox/share") { } else if (path === "/api/sandbox/share") {
if (method === "GET") { if (method === "GET") {
const params = url.searchParams const params = url.searchParams
if (params.has("id")) { if (params.has("id")) {
const id = params.get("id") as string const id = params.get("id") as string
const res = await db.query.usersToSandboxes.findMany({ const res = await db.query.usersToSandboxes.findMany({
where: (uts, { eq }) => eq(uts.userId, id), where: (uts, { eq }) => eq(uts.userId, id),
}) })
const owners = await Promise.all( const owners = await Promise.all(
res.map(async (r) => { res.map(async (r) => {
const sb = await db.query.sandbox.findFirst({ const sb = await db.query.sandbox.findFirst({
where: (sandbox, { eq }) => eq(sandbox.id, r.sandboxId), where: (sandbox, { eq }) => eq(sandbox.id, r.sandboxId),
with: { with: {
author: true, author: true,
}, },
}) })
if (!sb) return if (!sb) return
return { return {
id: sb.id, id: sb.id,
name: sb.name, name: sb.name,
type: sb.type, type: sb.type,
author: sb.author.name, author: sb.author.name,
authorAvatarUrl: sb.author.avatarUrl, authorAvatarUrl: sb.author.avatarUrl,
sharedOn: r.sharedOn, sharedOn: r.sharedOn,
} }
}) })
) )
return json(owners ?? {}) return json(owners ?? {})
} else return invalidRequest } else return invalidRequest
} else if (method === "POST") { } else if (method === "POST") {
const shareSchema = z.object({ const shareSchema = z.object({
sandboxId: z.string(), sandboxId: z.string(),
email: z.string(), email: z.string(),
}) })
const body = await request.json() const body = await request.json()
const { sandboxId, email } = shareSchema.parse(body) const { sandboxId, email } = shareSchema.parse(body)
const user = await db.query.user.findFirst({ const user = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.email, email), where: (user, { eq }) => eq(user.email, email),
with: { with: {
sandbox: true, sandbox: true,
usersToSandboxes: true, usersToSandboxes: true,
}, },
}) })
if (!user) { if (!user) {
return new Response("No user associated with email.", { status: 400 }) return new Response("No user associated with email.", { status: 400 })
} }
if (user.sandbox.find((sb) => sb.id === sandboxId)) { if (user.sandbox.find((sb) => sb.id === sandboxId)) {
return new Response("Cannot share with yourself!", { status: 400 }) return new Response("Cannot share with yourself!", { status: 400 })
} }
if (user.usersToSandboxes.find((uts) => uts.sandboxId === sandboxId)) { if (user.usersToSandboxes.find((uts) => uts.sandboxId === sandboxId)) {
return new Response("User already has access.", { status: 400 }) return new Response("User already has access.", { status: 400 })
} }
await db await db
.insert(usersToSandboxes) .insert(usersToSandboxes)
.values({ userId: user.id, sandboxId, sharedOn: new Date() }) .values({ userId: user.id, sandboxId, sharedOn: new Date() })
.get() .get()
return success return success
} else if (method === "DELETE") { } else if (method === "DELETE") {
const deleteShareSchema = z.object({ const deleteShareSchema = z.object({
sandboxId: z.string(), sandboxId: z.string(),
userId: z.string(), userId: z.string(),
}) })
const body = await request.json() const body = await request.json()
const { sandboxId, userId } = deleteShareSchema.parse(body) const { sandboxId, userId } = deleteShareSchema.parse(body)
await db await db
.delete(usersToSandboxes) .delete(usersToSandboxes)
.where( .where(
and( and(
eq(usersToSandboxes.userId, userId), eq(usersToSandboxes.userId, userId),
eq(usersToSandboxes.sandboxId, sandboxId) eq(usersToSandboxes.sandboxId, sandboxId)
) )
) )
return success return success
} else return methodNotAllowed } else return methodNotAllowed
} else if (path === "/api/sandbox/like") { } else if (path === "/api/user") {
if (method === "POST") { if (method === "GET") {
const likeSchema = z.object({ const params = url.searchParams
sandboxId: z.string(),
userId: z.string(),
})
try { if (params.has("id")) {
const body = await request.json() const id = params.get("id") as string
const { sandboxId, userId } = likeSchema.parse(body) const res = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.id, id),
with: {
sandbox: {
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
},
usersToSandboxes: true,
},
})
return json(res ?? {})
} else {
const res = await db.select().from(user).all()
return json(res ?? {})
}
} else if (method === "POST") {
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
username: z.string(),
avatarUrl: z.string().optional(),
createdAt: z.string().optional(),
generations: z.number().optional(),
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]).optional(),
tierExpiresAt: z.number().optional(),
})
// Check if user has already liked const body = await request.json()
const existingLike = await db.query.sandboxLikes.findFirst({ const { id, name, email, username, avatarUrl, createdAt, generations, tier, tierExpiresAt } = userSchema.parse(body)
where: (likes, { and, eq }) =>
and(eq(likes.sandboxId, sandboxId), eq(likes.userId, userId)),
})
if (existingLike) { const res = await db
// Unlike .insert(user)
await db .values({
.delete(sandboxLikes) id,
.where( name,
and( email,
eq(sandboxLikes.sandboxId, sandboxId), username,
eq(sandboxLikes.userId, userId) avatarUrl,
) createdAt: createdAt ? new Date(createdAt) : new Date(),
) generations,
tier,
tierExpiresAt,
})
.returning()
.get()
return json({ res })
} else if (method === "DELETE") {
const params = url.searchParams
if (params.has("id")) {
const id = params.get("id") as string
await db.delete(user).where(eq(user.id, id))
return success
} else return invalidRequest
} else {
return methodNotAllowed
}
} else if (path === "/api/user/check-username") {
if (method === "GET") {
const params = url.searchParams
const username = params.get("username")
await db if (!username) return invalidRequest
.update(sandbox)
.set({
likeCount: sql`${sandbox.likeCount} - 1`,
})
.where(eq(sandbox.id, sandboxId))
return json({ const exists = await db.query.user.findFirst({
message: "Unlike successful", where: (user, { eq }) => eq(user.username, username)
liked: false, })
})
} else {
// Like
await db.insert(sandboxLikes).values({
sandboxId,
userId,
createdAt: new Date(),
})
await db return json({ exists: !!exists })
.update(sandbox) }
.set({ return methodNotAllowed
likeCount: sql`${sandbox.likeCount} + 1`, } else if (path === "/api/user/increment-generations" && method === "POST") {
}) const schema = z.object({
.where(eq(sandbox.id, sandboxId)) userId: z.string(),
})
return json({ const body = await request.json()
message: "Like successful", const { userId } = schema.parse(body)
liked: true,
})
}
} catch (error) {
return new Response("Invalid request format", { status: 400 })
}
} else if (method === "GET") {
const params = url.searchParams
const sandboxId = params.get("sandboxId")
const userId = params.get("userId")
if (!sandboxId || !userId) { await db
return invalidRequest .update(user)
} .set({ generations: sql`${user.generations} + 1` })
.where(eq(user.id, userId))
.get()
const like = await db.query.sandboxLikes.findFirst({ return success
where: (likes, { and, eq }) => } else if (path === "/api/user/update-tier" && method === "POST") {
and(eq(likes.sandboxId, sandboxId), eq(likes.userId, userId)), const schema = z.object({
}) userId: z.string(),
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]),
tierExpiresAt: z.date(),
})
return json({ const body = await request.json()
liked: !!like, const { userId, tier, tierExpiresAt } = schema.parse(body)
})
} else {
return methodNotAllowed
}
} else if (path === "/api/user") {
if (method === "GET") {
const params = url.searchParams
if (params.has("id")) { await db
const id = params.get("id") as string .update(user)
.set({
tier,
tierExpiresAt: tierExpiresAt.getTime(),
// Reset generations when upgrading tier
generations: 0
})
.where(eq(user.id, userId))
.get()
const res = await db.query.user.findFirst({ return success
where: (user, { eq }) => eq(user.id, id), } else if (path === "/api/user/check-reset" && method === "POST") {
with: { const schema = z.object({
sandbox: { userId: z.string(),
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)], })
with: {
likes: true,
},
},
usersToSandboxes: true,
},
})
if (res) {
const transformedUser: UserResponse = {
...res,
sandbox: res.sandbox.map(
(sb): SandboxWithLiked => ({
...sb,
liked: sb.likes.some((like) => like.userId === id),
})
),
}
return json(transformedUser)
}
return json(res ?? {})
} else if (params.has("username")) {
const username = params.get("username") as string
const userId = params.get("currentUserId")
const res = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.username, username),
with: {
sandbox: {
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
with: {
likes: true,
},
},
usersToSandboxes: true,
},
})
if (res) {
const transformedUser: UserResponse = {
...res,
sandbox: res.sandbox.map(
(sb): SandboxWithLiked => ({
...sb,
liked: sb.likes.some((like) => like.userId === userId),
})
),
}
return json(transformedUser)
}
return json(res ?? {})
} else {
const res = await db.select().from(user).all()
return json(res ?? {})
}
} else if (method === "POST") {
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
username: z.string(),
avatarUrl: z.string().optional(),
githubToken: z.string().nullable().optional(),
createdAt: z.string().optional(),
generations: z.number().optional(),
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]).optional(),
tierExpiresAt: z.number().optional(),
lastResetDate: z.number().optional(),
})
const body = await request.json() const body = await request.json()
const { userId } = schema.parse(body)
const { const dbUser = await db.query.user.findFirst({
id, where: (user, { eq }) => eq(user.id, userId),
name, })
email,
username,
avatarUrl,
githubToken,
createdAt,
generations,
tier,
tierExpiresAt,
lastResetDate,
} = userSchema.parse(body)
const res = await db
.insert(user)
.values({
id,
name,
email,
username,
avatarUrl,
githubToken,
createdAt: createdAt ? new Date(createdAt) : new Date(),
generations,
tier,
tierExpiresAt,
lastResetDate,
})
.returning()
.get()
return json({ res })
} else if (method === "DELETE") {
const params = url.searchParams
if (params.has("id")) {
const id = params.get("id") as string
await db.delete(user).where(eq(user.id, id))
return success
} else return invalidRequest
} else if (method === "PUT") {
const updateUserSchema = z.object({
id: z.string(),
name: z.string().optional(),
bio: z.string().optional(),
personalWebsite: z.string().optional(),
links: z
.array(
z.object({
url: z.string(),
platform: z.enum(schema.KNOWN_PLATFORMS),
})
)
.optional(),
email: z.string().email().optional(),
username: z.string().optional(),
avatarUrl: z.string().optional(),
githubToken: z.string().nullable().optional(),
generations: z.number().optional(),
})
try { if (!dbUser) {
const body = await request.json() return new Response("User not found", { status: 404 })
const validatedData = updateUserSchema.parse(body) }
const { id, username, ...updateData } = validatedData const now = new Date()
const lastReset = dbUser.lastResetDate ? new Date(dbUser.lastResetDate) : new Date(0)
// If username is being updated, check for existing username if (now.getMonth() !== lastReset.getMonth() || now.getFullYear() !== lastReset.getFullYear()) {
if (username) { await db
const existingUser = await db .update(user)
.select() .set({
.from(user) generations: 0,
.where(eq(user.username, username)) lastResetDate: now.getTime()
.get() })
if (existingUser && existingUser.id !== id) { .where(eq(user.id, userId))
return json({ error: "Username already exists" }, { status: 409 }) .get()
}
}
const cleanUpdateData = { return new Response("Reset successful", { status: 200 })
...updateData, }
...(username ? { username } : {}),
}
const res = await db return new Response("No reset needed", { status: 200 })
.update(user) } else return notFound
.set(cleanUpdateData) },
.where(eq(user.id, id))
.returning()
.get()
if (!res) {
return json({ error: "User not found" }, { status: 404 })
}
return json({ res })
} catch (error) {
if (error instanceof z.ZodError) {
return json({ error: error.errors }, { status: 400 })
}
return json({ error: "Internal server error" }, { status: 500 })
}
} else {
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 if (
path === "/api/user/increment-generations" &&
method === "POST"
) {
const schema = z.object({
userId: z.string(),
})
const body = await request.json()
const { userId } = schema.parse(body)
await db
.update(user)
.set({ generations: sql`${user.generations} + 1` })
.where(eq(user.id, userId))
.get()
return success
} else if (path === "/api/user/update-tier" && method === "POST") {
const schema = z.object({
userId: z.string(),
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]),
tierExpiresAt: z.date(),
})
const body = await request.json()
const { userId, tier, tierExpiresAt } = schema.parse(body)
await db
.update(user)
.set({
tier,
tierExpiresAt: tierExpiresAt.getTime(),
// Reset generations when upgrading tier
generations: 0,
})
.where(eq(user.id, userId))
.get()
return success
} else if (path === "/api/user/check-reset" && method === "POST") {
const schema = z.object({
userId: z.string(),
})
const body = await request.json()
const { userId } = schema.parse(body)
const dbUser = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.id, userId),
})
if (!dbUser) {
return new Response("User not found", { status: 404 })
}
const now = new Date()
const lastReset = dbUser.lastResetDate
? new Date(dbUser.lastResetDate)
: new Date(0)
if (
now.getMonth() !== lastReset.getMonth() ||
now.getFullYear() !== lastReset.getFullYear()
) {
await db
.update(user)
.set({
generations: 0,
lastResetDate: now.getTime(),
})
.where(eq(user.id, userId))
.get()
return new Response("Reset successful", { status: 200 })
}
return new Response("No reset needed", { status: 200 })
} else return notFound
},
} }

View File

@ -1,140 +1,79 @@
import { createId } from "@paralleldrive/cuid2" import { createId } from "@paralleldrive/cuid2"
import { relations, sql } from "drizzle-orm" import { relations } from "drizzle-orm"
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core" import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
import { sql } from "drizzle-orm"
export const KNOWN_PLATFORMS = [
"github",
"twitter",
"instagram",
"bluesky",
"linkedin",
"youtube",
"twitch",
"discord",
"mastodon",
"threads",
"gitlab",
"generic",
] as const
export type KnownPlatform = (typeof KNOWN_PLATFORMS)[number]
export type UserLink = {
url: string
platform: KnownPlatform
}
// #region Tables
export const user = sqliteTable("user", { export const user = sqliteTable("user", {
id: text("id") id: text("id")
.$defaultFn(() => createId()) .$defaultFn(() => createId())
.primaryKey() .primaryKey()
.unique(), .unique(),
name: text("name").notNull(), name: text("name").notNull(),
email: text("email").notNull(), email: text("email").notNull(),
username: text("username").notNull().unique(), username: text("username").notNull().unique(),
avatarUrl: text("avatarUrl"), avatarUrl: text("avatarUrl"),
githubToken: text("githubToken"), createdAt: integer("createdAt", { mode: "timestamp_ms" })
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default( .default(sql`CURRENT_TIMESTAMP`),
sql`CURRENT_TIMESTAMP` generations: integer("generations").default(0),
), tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
generations: integer("generations").default(0), tierExpiresAt: integer("tierExpiresAt"),
bio: text("bio"), lastResetDate: integer("lastResetDate"),
personalWebsite: text("personalWebsite"),
links: text("links", { mode: "json" }).default("[]").$type<UserLink[]>(),
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
tierExpiresAt: integer("tierExpiresAt"),
lastResetDate: integer("lastResetDate"),
}) })
export type User = typeof user.$inferSelect export type User = typeof user.$inferSelect
export const userRelations = relations(user, ({ many }) => ({
sandbox: many(sandbox),
usersToSandboxes: many(usersToSandboxes),
}))
export const sandbox = sqliteTable("sandbox", { export const sandbox = sqliteTable("sandbox", {
id: text("id") id: text("id")
.$defaultFn(() => createId()) .$defaultFn(() => createId())
.primaryKey() .primaryKey()
.unique(), .unique(),
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" }).default( createdAt: integer("createdAt", { mode: "timestamp_ms" })
sql`CURRENT_TIMESTAMP` .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),
likeCount: integer("likeCount").default(0), viewCount: integer("viewCount").default(0),
viewCount: integer("viewCount").default(0),
containerId: text("containerId"),
}) })
export type Sandbox = typeof sandbox.$inferSelect export type Sandbox = typeof sandbox.$inferSelect
export const sandboxLikes = sqliteTable( export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
"sandbox_likes", author: one(user, {
{ fields: [sandbox.userId],
userId: text("user_id") references: [user.id],
.notNull() }),
.references(() => user.id), usersToSandboxes: many(usersToSandboxes),
sandboxId: text("sandbox_id") }))
.notNull()
.references(() => sandbox.id),
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
sql`CURRENT_TIMESTAMP`
),
},
(table) => ({
pk: primaryKey({ columns: [table.sandboxId, table.userId] }),
})
)
export const usersToSandboxes = sqliteTable("users_to_sandboxes", { export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => user.id), .references(() => user.id),
sandboxId: text("sandboxId") sandboxId: text("sandboxId")
.notNull() .notNull()
.references(() => sandbox.id), .references(() => sandbox.id),
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }), sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
}) })
// #region Relations
export const userRelations = relations(user, ({ many }) => ({
sandbox: many(sandbox),
usersToSandboxes: many(usersToSandboxes),
likes: many(sandboxLikes),
}))
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
author: one(user, {
fields: [sandbox.userId],
references: [user.id],
}),
usersToSandboxes: many(usersToSandboxes),
likes: many(sandboxLikes),
}))
export const sandboxLikesRelations = relations(sandboxLikes, ({ one }) => ({
user: one(user, {
fields: [sandboxLikes.userId],
references: [user.id],
}),
sandbox: one(sandbox, {
fields: [sandboxLikes.sandboxId],
references: [sandbox.id],
}),
}))
export const usersToSandboxesRelations = relations( export const usersToSandboxesRelations = relations(
usersToSandboxes, usersToSandboxes,
({ one }) => ({ ({ one }) => ({
group: one(sandbox, { group: one(sandbox, {
fields: [usersToSandboxes.sandboxId], fields: [usersToSandboxes.sandboxId],
references: [sandbox.id], references: [sandbox.id],
}), }),
user: one(user, { user: one(user, {
fields: [usersToSandboxes.userId], fields: [usersToSandboxes.userId],
references: [user.id], references: [user.id],
}), }),
}) })
) )
// #endregion

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

@ -33,9 +33,9 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.0", "version": "7.25.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -111,11 +111,11 @@
} }
}, },
"node_modules/@kwsites/file-exists/node_modules/debug": { "node_modules/@kwsites/file-exists/node_modules/debug": {
"version": "4.3.7", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -127,9 +127,9 @@
} }
}, },
"node_modules/@kwsites/file-exists/node_modules/ms": { "node_modules/@kwsites/file-exists/node_modules/ms": {
"version": "2.1.3", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/@kwsites/promise-deferred": { "node_modules/@kwsites/promise-deferred": {
"version": "1.1.1", "version": "1.1.1",
@ -210,9 +210,9 @@
} }
}, },
"node_modules/@types/express-serve-static-core": { "node_modules/@types/express-serve-static-core": {
"version": "4.19.6", "version": "4.19.5",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz",
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
@ -244,17 +244,17 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.17.6", "version": "20.14.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
"integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==", "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
"dependencies": { "dependencies": {
"undici-types": "~6.19.2" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.17", "version": "6.9.15",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==",
"dev": true "dev": true
}, },
"node_modules/@types/range-parser": { "node_modules/@types/range-parser": {
@ -294,20 +294,14 @@
} }
}, },
"node_modules/@types/ssh2/node_modules/@types/node": { "node_modules/@types/ssh2/node_modules/@types/node": {
"version": "18.19.64", "version": "18.19.44",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.44.tgz",
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==", "integrity": "sha512-ZsbGerYg72WMXUIE9fYxtvfzLEuq6q8mKERdWFnqTmOvudMxnz+CBNRoOwJ2kNpFOncrKjT1hZwxjlFgQ9qvQA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/@types/ssh2/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -321,9 +315,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.0", "version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@ -333,9 +327,9 @@
} }
}, },
"node_modules/acorn-walk": { "node_modules/acorn-walk": {
"version": "8.3.4", "version": "8.3.3",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"acorn": "^8.11.0" "acorn": "^8.11.0"
@ -433,9 +427,9 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@ -445,7 +439,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.13.0", "qs": "6.11.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@ -477,6 +471,20 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bufferutil": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/buildcheck": { "node_modules/buildcheck": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
@ -648,9 +656,9 @@
} }
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.7.1", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -801,24 +809,24 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "2.0.0", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/engine.io": { "node_modules/engine.io": {
"version": "6.6.2", "version": "6.5.5",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz",
"integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==",
"dependencies": { "dependencies": {
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/node": ">=10.0.0", "@types/node": ">=10.0.0",
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "2.0.0", "base64id": "2.0.0",
"cookie": "~0.7.2", "cookie": "~0.4.1",
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
@ -837,19 +845,19 @@
} }
}, },
"node_modules/engine.io/node_modules/cookie": { "node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/engine.io/node_modules/debug": { "node_modules/engine.io/node_modules/debug": {
"version": "4.3.7", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -861,9 +869,29 @@
} }
}, },
"node_modules/engine.io/node_modules/ms": { "node_modules/engine.io/node_modules/ms": {
"version": "2.1.3", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/engine.io/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}, },
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.0",
@ -885,9 +913,9 @@
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -906,36 +934,36 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.1", "version": "4.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "1.20.2",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.7.1", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.3.1", "finalhandler": "1.2.0",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.3", "merge-descriptors": "1.0.1",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.10", "path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "6.11.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.19.0", "send": "0.18.0",
"serve-static": "1.16.2", "serve-static": "1.15.0",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@ -959,12 +987,12 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.3.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@ -1267,12 +1295,9 @@
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.3", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/methods": { "node_modules/methods": {
"version": "1.1.2", "version": "1.1.2",
@ -1330,9 +1355,9 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"node_modules/nan": { "node_modules/nan": {
"version": "2.22.0", "version": "2.20.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
"optional": true "optional": true
}, },
"node_modules/negotiator": { "node_modules/negotiator": {
@ -1343,10 +1368,22 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-gyp-build": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
"integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
"optional": true,
"peer": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.7", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz",
"integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"chokidar": "^3.5.2", "chokidar": "^3.5.2",
@ -1372,12 +1409,12 @@
} }
}, },
"node_modules/nodemon/node_modules/debug": { "node_modules/nodemon/node_modules/debug": {
"version": "4.3.7", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -1398,9 +1435,9 @@
} }
}, },
"node_modules/nodemon/node_modules/ms": { "node_modules/nodemon/node_modules/ms": {
"version": "2.1.3", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true "dev": true
}, },
"node_modules/nodemon/node_modules/supports-color": { "node_modules/nodemon/node_modules/supports-color": {
@ -1433,9 +1470,9 @@
} }
}, },
"node_modules/object-inspect": { "node_modules/object-inspect": {
"version": "1.13.3", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
@ -1482,9 +1519,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.10", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -1528,11 +1565,11 @@
"dev": true "dev": true
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": { "dependencies": {
"side-channel": "^1.0.6" "side-channel": "^1.0.4"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@ -1550,9 +1587,9 @@
} }
}, },
"node_modules/rate-limiter-flexible": { "node_modules/rate-limiter-flexible": {
"version": "5.0.4", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-5.0.4.tgz", "resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-5.0.3.tgz",
"integrity": "sha512-ftYHrIfSqWYDIJZ4yPTrgOduByAp+86gUS9iklv0JoXVM8eQCAjTnydCj1hAT4MmhmkSw86NaFEJ28m/LC1pKA==" "integrity": "sha512-lWx2y8NBVlTOLPyqs+6y7dxfEpT6YFqKy3MzWbCy95sTTOhOuxufP2QvRyOHpfXpB9OUJPbVLybw3z3AVAS5fA=="
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "2.5.2", "version": "2.5.2",
@ -1659,9 +1696,9 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.19.0", "version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -1681,28 +1718,20 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/send/node_modules/ms": { "node_modules/send/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.16.2", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": { "dependencies": {
"encodeurl": "~2.0.0", "encodeurl": "~1.0.2",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.19.0" "send": "0.18.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@ -1761,9 +1790,9 @@
} }
}, },
"node_modules/simple-git": { "node_modules/simple-git": {
"version": "3.27.0", "version": "3.25.0",
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz", "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.25.0.tgz",
"integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==", "integrity": "sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw==",
"dependencies": { "dependencies": {
"@kwsites/file-exists": "^1.1.1", "@kwsites/file-exists": "^1.1.1",
"@kwsites/promise-deferred": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1",
@ -1775,11 +1804,11 @@
} }
}, },
"node_modules/simple-git/node_modules/debug": { "node_modules/simple-git/node_modules/debug": {
"version": "4.3.7", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -1791,9 +1820,9 @@
} }
}, },
"node_modules/simple-git/node_modules/ms": { "node_modules/simple-git/node_modules/ms": {
"version": "2.1.3", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/simple-update-notifier": { "node_modules/simple-update-notifier": {
"version": "2.0.0", "version": "2.0.0",
@ -1808,15 +1837,15 @@
} }
}, },
"node_modules/socket.io": { "node_modules/socket.io": {
"version": "4.8.1", "version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
"dependencies": { "dependencies": {
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "~2.0.0", "base64id": "~2.0.0",
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.3.2", "debug": "~4.3.2",
"engine.io": "~6.6.0", "engine.io": "~6.5.2",
"socket.io-adapter": "~2.5.2", "socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4" "socket.io-parser": "~4.2.4"
}, },
@ -1834,11 +1863,11 @@
} }
}, },
"node_modules/socket.io-adapter/node_modules/debug": { "node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -1850,9 +1879,29 @@
} }
}, },
"node_modules/socket.io-adapter/node_modules/ms": { "node_modules/socket.io-adapter/node_modules/ms": {
"version": "2.1.3", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/socket.io-adapter/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}, },
"node_modules/socket.io-parser": { "node_modules/socket.io-parser": {
"version": "4.2.4", "version": "4.2.4",
@ -1867,11 +1916,11 @@
} }
}, },
"node_modules/socket.io-parser/node_modules/debug": { "node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -1883,16 +1932,16 @@
} }
}, },
"node_modules/socket.io-parser/node_modules/ms": { "node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.3", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/socket.io/node_modules/debug": { "node_modules/socket.io/node_modules/debug": {
"version": "4.3.7", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "2.1.2"
}, },
"engines": { "engines": {
"node": ">=6.0" "node": ">=6.0"
@ -1904,9 +1953,9 @@
} }
}, },
"node_modules/socket.io/node_modules/ms": { "node_modules/socket.io/node_modules/ms": {
"version": "2.1.3", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/spawn-command": { "node_modules/spawn-command": {
"version": "0.0.2", "version": "0.0.2",
@ -1914,9 +1963,9 @@
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
}, },
"node_modules/ssh2": { "node_modules/ssh2": {
"version": "1.16.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz",
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"asn1": "^0.2.6", "asn1": "^0.2.6",
@ -1926,8 +1975,8 @@
"node": ">=10.16.0" "node": ">=10.16.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"cpu-features": "~0.0.10", "cpu-features": "~0.0.9",
"nan": "^2.20.0" "nan": "^2.18.0"
} }
}, },
"node_modules/statuses": { "node_modules/statuses": {
@ -2072,9 +2121,9 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.8.1", "version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
}, },
"node_modules/tweetnacl": { "node_modules/tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
@ -2094,9 +2143,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.6.3", "version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@ -2113,9 +2162,9 @@
"dev": true "dev": true
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.19.8", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
}, },
"node_modules/unpipe": { "node_modules/unpipe": {
"version": "1.0.0", "version": "1.0.0",
@ -2125,6 +2174,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/utf-8-validate": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.4.tgz",
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2169,26 +2232,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
} }
}, },
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -31,30 +31,9 @@ export class DokkuClient extends SSHSocketClient {
// List all deployed Dokku apps // List all deployed Dokku apps
async listApps(): Promise<string[]> { async listApps(): Promise<string[]> {
const response = await this.sendCommand("--quiet apps:list") const response = await this.sendCommand("apps:list")
return response.output.split("\n") // Split the output by newline and remove the header
} return response.output.split("\n").slice(1)
// Get the creation timestamp of an app
async getAppCreatedAt(appName: string): Promise<number> {
const response = await this.sendCommand(
`apps:report --app-created-at ${appName}`
)
const createdAt = parseInt(response.output.trim(), 10)
if (isNaN(createdAt)) {
throw new Error(
`Failed to retrieve creation timestamp for app ${appName}`
)
}
return createdAt
}
// Check if an app exists
async appExists(appName: string): Promise<boolean> {
const response = await this.sendCommand(`apps:exists ${appName}`)
return response.output.includes("App") === false
} }
} }

View File

@ -92,11 +92,7 @@ export class FileManager {
// Convert local file path to remote path // Convert local file path to remote path
private getRemoteFileId(localId: string): string { private getRemoteFileId(localId: string): string {
return [ return `projects/${this.sandboxId}${localId}`
"projects",
this.sandboxId,
localId.startsWith("/") ? localId : localId,
].join("/")
} }
// Convert remote file path to local file path // Convert remote file path to local file path
@ -330,15 +326,13 @@ export class FileManager {
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) { if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
throw new Error("File size too large. Please reduce the file size.") throw new Error("File size too large. Please reduce the file size.")
} }
// Save to remote storage
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body) await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
// Update local file data cache
let file = this.fileData.find((f) => f.id === fileId) let file = this.fileData.find((f) => f.id === fileId)
if (file) { if (file) {
file.data = body file.data = body
} else { } else {
// If the file wasn't in our cache, add it
file = { file = {
id: fileId, id: fileId,
data: body, data: body,
@ -346,49 +340,7 @@ export class FileManager {
this.fileData.push(file) this.fileData.push(file)
} }
// Save to sandbox filesystem await this.sandbox.files.write(path.posix.join(this.dirName, fileId), body)
const filePath = path.posix.join(this.dirName, fileId)
await this.sandbox.files.write(filePath, body)
// Instead of updating the entire file structure, just ensure this file exists in it
const parts = fileId.split('/').filter(Boolean)
let current = this.files
let currentPath = ''
// Navigate/create the path to the file
for (let i = 0; i < parts.length - 1; i++) {
currentPath += '/' + parts[i]
let folder = current.find(
(f) => f.type === 'folder' && f.name === parts[i]
) as TFolder
if (!folder) {
folder = {
id: currentPath,
type: 'folder',
name: parts[i],
children: [],
}
current.push(folder)
}
current = folder.children
}
// Add/update the file in the structure if it doesn't exist
const fileName = parts[parts.length - 1]
const existingFile = current.find(
(f) => f.type === 'file' && f.name === fileName
)
if (!existingFile) {
current.push({
id: fileId,
type: 'file',
name: fileName,
})
}
this.refreshFileList?.(this.files)
this.fixPermissions() this.fixPermissions()
} }

View File

@ -13,10 +13,7 @@ export class SSHSocketClient {
private conn: Client private conn: Client
private config: SSHConfig private config: SSHConfig
private socketPath: string private socketPath: string
private _isConnected: boolean = false private isConnected: boolean = false
public get isConnected(): boolean {
return this._isConnected
}
// Constructor initializes the SSH client and sets up configuration // Constructor initializes the SSH client and sets up configuration
constructor(config: SSHConfig, socketPath: string) { constructor(config: SSHConfig, socketPath: string) {
@ -37,7 +34,7 @@ export class SSHSocketClient {
private closeConnection() { private closeConnection() {
console.log("Closing SSH connection...") console.log("Closing SSH connection...")
this.conn.end() this.conn.end()
this._isConnected = false this.isConnected = false
process.exit(0) process.exit(0)
} }
@ -47,17 +44,17 @@ export class SSHSocketClient {
this.conn this.conn
.on("ready", () => { .on("ready", () => {
console.log("SSH connection established") console.log("SSH connection established")
this._isConnected = true this.isConnected = true
resolve() resolve()
}) })
.on("error", (err) => { .on("error", (err) => {
console.error("SSH connection error:", err) console.error("SSH connection error:", err)
this._isConnected = false this.isConnected = false
reject(err) reject(err)
}) })
.on("close", () => { .on("close", () => {
console.log("SSH connection closed") console.log("SSH connection closed")
this._isConnected = false this.isConnected = false
}) })
.connect(this.config) .connect(this.config)
}) })
@ -89,13 +86,10 @@ export class SSHSocketClient {
) )
}) })
.on("data", (data: Buffer) => { .on("data", (data: Buffer) => {
// Netcat remains open until it is closed, so we close the connection once we receive data.
resolve(data.toString()) resolve(data.toString())
stream.close()
}) })
.stderr.on("data", (data: Buffer) => { .stderr.on("data", (data: Buffer) => {
reject(new Error(data.toString())) reject(new Error(data.toString()))
stream.close()
}) })
} }
) )

View File

@ -71,13 +71,7 @@ export class Sandbox {
} else { } else {
console.log("Creating container", this.sandboxId) console.log("Creating container", this.sandboxId)
// Create a new container with a specified template and timeout // Create a new container with a specified template and timeout
const templateTypes = [ const templateTypes = ["vanillajs", "reactjs", "nextjs", "streamlit"]
"vanillajs",
"reactjs",
"nextjs",
"streamlit",
"php",
]
const template = templateTypes.includes(this.type) const template = templateTypes.includes(this.type)
? `gitwit-${this.type}` ? `gitwit-${this.type}`
: `base` : `base`
@ -156,40 +150,6 @@ export class Sandbox {
return { success: true, apps: await this.dokkuClient.listApps() } return { success: true, apps: await this.dokkuClient.listApps() }
} }
// Handle getting app creation timestamp
const handleGetAppCreatedAt: SocketHandler = async ({ appName }) => {
if (!this.dokkuClient)
throw new Error(
"Failed to retrieve app creation timestamp: No Dokku client"
)
return {
success: true,
createdAt: await this.dokkuClient.getAppCreatedAt(appName),
}
}
// Handle checking if an app exists
const handleAppExists: SocketHandler = async ({ appName }) => {
if (!this.dokkuClient) {
console.log("Failed to check app existence: No Dokku client")
return {
success: false,
}
}
if (!this.dokkuClient.isConnected) {
console.log(
"Failed to check app existence: The Dokku client is not connected"
)
return {
success: false,
}
}
return {
success: true,
exists: await this.dokkuClient.appExists(appName),
}
}
// Handle deploying code // Handle deploying code
const handleDeploy: SocketHandler = async (_: any) => { const handleDeploy: SocketHandler = async (_: any) => {
if (!this.gitClient) throw Error("No git client") if (!this.gitClient) throw Error("No git client")
@ -287,9 +247,7 @@ export class Sandbox {
getFolder: handleGetFolder, getFolder: handleGetFolder,
saveFile: handleSaveFile, saveFile: handleSaveFile,
moveFile: handleMoveFile, moveFile: handleMoveFile,
listApps: handleListApps, list: handleListApps,
getAppCreatedAt: handleGetAppCreatedAt,
getAppExists: handleAppExists,
deploy: handleDeploy, deploy: handleDeploy,
createFile: handleCreateFile, createFile: handleCreateFile,
createFolder: handleCreateFolder, createFolder: handleCreateFolder,

View File

@ -146,8 +146,6 @@ io.on("connection", async (socket) => {
) )
}) })
socket.emit("ready")
// Handle disconnection event // Handle disconnection event
socket.on("disconnect", async () => { socket.on("disconnect", async () => {
try { try {

View File

@ -3,21 +3,18 @@ CLERK_SECRET_KEY=
NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY= NEXT_PUBLIC_LIVEBLOCKS_PUBLIC_KEY=
LIVEBLOCKS_SECRET_KEY= LIVEBLOCKS_SECRET_KEY=
NEXT_PUBLIC_SERVER_PORT=4000
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_SERVER_URL=http://localhost:4000 NEXT_PUBLIC_SERVER_URL=http://localhost:4000
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.
NEXT_PUBLIC_DATABASE_WORKER_URL=https://database.your-worker.workers.dev # These URLs should begin with https:// in production
NEXT_PUBLIC_STORAGE_WORKER_URL=https://storage.your-worker.workers.dev NEXT_PUBLIC_DATABASE_WORKER_URL=
NEXT_PUBLIC_WORKERS_KEY=SUPERDUPERSECRET NEXT_PUBLIC_STORAGE_WORKER_URL=
NEXT_PUBLIC_AI_WORKER_URL=
NEXT_PUBLIC_WORKERS_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
ANTHROPIC_API_KEY=
OPENAI_API_KEY=

View File

@ -91,17 +91,23 @@ export default async function CodePage({ params }: { params: { id: string } }) {
} }
return ( return (
<TerminalProvider> <>
{/* <Room id={sandboxId}> */} <div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<div className="overflow-hidden overscroll-none w-screen h-screen grid [grid-template-rows:3.5rem_auto] bg-background"> {/* <Room id={sandboxId}> */}
<Navbar <TerminalProvider>
userData={userData} <Navbar
sandboxData={sandboxData} userData={userData}
shared={shared as { id: string; name: string; avatarUrl: string }[]} sandboxData={sandboxData}
/> shared={
<CodeEditor userData={userData} sandboxData={sandboxData} /> shared as { id: string; name: string; avatarUrl: string }[]
}
/>
<div className="w-screen flex grow">
<CodeEditor userData={userData} sandboxData={sandboxData} />
</div>
</TerminalProvider>
{/* </Room> */}
</div> </div>
{/* </Room> */} </>
</TerminalProvider>
) )
} }

View File

@ -1,65 +0,0 @@
import ProfilePage from "@/components/profile"
import ProfileNavbar from "@/components/profile/navbar"
import { SandboxWithLiked, User } from "@/lib/types"
import { currentUser } from "@clerk/nextjs"
import { notFound } from "next/navigation"
export default async function Page({
params: { username: rawUsername },
}: {
params: { username: string }
}) {
const username = decodeURIComponent(rawUsername).replace("@", "")
const loggedInClerkUser = await currentUser()
const [profileOwnerResponse, loggedInUserResponse] = await Promise.all([
fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?username=${username}&currentUserId=${loggedInClerkUser?.id}`,
{
headers: {
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
},
}
),
fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${loggedInClerkUser?.id}`,
{
headers: {
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
},
}
),
])
const profileOwner = (await profileOwnerResponse.json()) as User
const loggedInUser = (await loggedInUserResponse.json()) as User
if (!Boolean(profileOwner?.id)) {
notFound()
}
const publicSandboxes: SandboxWithLiked[] = []
const privateSandboxes: SandboxWithLiked[] = []
profileOwner?.sandbox?.forEach((sandbox) => {
if (sandbox.visibility === "public") {
publicSandboxes.push(sandbox as SandboxWithLiked)
} else if (sandbox.visibility === "private") {
privateSandboxes.push(sandbox as SandboxWithLiked)
}
})
const isUserLoggedIn = Boolean(loggedInUser?.id)
return (
<section>
<ProfileNavbar userData={loggedInUser} />
<ProfilePage
publicSandboxes={publicSandboxes}
privateSandboxes={
profileOwner?.id === loggedInUser.id ? privateSandboxes : []
}
profileOwner={profileOwner}
loggedInUser={isUserLoggedIn ? loggedInUser : null}
/>
</section>
)
}

View File

@ -1,59 +1,12 @@
import {
ignoredFiles,
ignoredFolders,
} from "@/components/editor/AIChat/lib/ignored-paths"
import { templateConfigs } from "@/lib/templates"
import { TIERS } from "@/lib/tiers"
import { TFile, TFolder } from "@/lib/types"
import { Anthropic } from "@anthropic-ai/sdk"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
import { Anthropic } from "@anthropic-ai/sdk"
import { TIERS } from "@/lib/tiers"
import { templateConfigs } from "@/lib/templates"
const anthropic = new Anthropic({ const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY!, apiKey: process.env.ANTHROPIC_API_KEY!,
}) })
// Format file structure for context
function formatFileStructure(
items: (TFile | TFolder)[] | undefined,
prefix = ""
): string {
if (!items || !Array.isArray(items)) {
return "No files available"
}
// Sort items to show folders first, then files
const sortedItems = [...items].sort((a, b) => {
if (a.type === b.type) return a.name.localeCompare(b.name)
return a.type === "folder" ? -1 : 1
})
return sortedItems
.map((item) => {
if (
item.type === "file" &&
!ignoredFiles.some(
(pattern) =>
item.name.endsWith(pattern.replace("*", "")) ||
item.name === pattern
)
) {
return `${prefix}├── ${item.name}`
} else if (
item.type === "folder" &&
!ignoredFolders.some((folder) => folder === item.name)
) {
const folderContent = formatFileStructure(
item.children,
`${prefix}`
)
return `${prefix}├── ${item.name}/\n${folderContent}`
}
return null
})
.filter(Boolean)
.join("\n")
}
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
const user = await currentUser() const user = await currentUser()
@ -90,8 +43,7 @@ export async function POST(request: Request) {
const userData = await dbUser.json() const userData = await dbUser.json()
// Get tier settings // Get tier settings
const tierSettings = const tierSettings = TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
if (userData.generations >= tierSettings.generations) { if (userData.generations >= tierSettings.generations) {
return new Response( return new Response(
`AI generation limit reached for your ${userData.tier || "FREE"} tier`, `AI generation limit reached for your ${userData.tier || "FREE"} tier`,
@ -106,32 +58,27 @@ export async function POST(request: Request) {
isEditMode, isEditMode,
fileName, fileName,
line, line,
templateType, templateType
files,
projectName,
} = await request.json() } = await request.json()
// Get template configuration // Get template configuration
const templateConfig = templateConfigs[templateType] const templateConfig = templateConfigs[templateType]
// Create template context // Create template context
const templateContext = templateConfig const templateContext = templateConfig ? `
? `
Project Template: ${templateConfig.name} Project Template: ${templateConfig.name}
Current File Structure: File Structure:
${files ? formatFileStructure(files) : "No files available"} ${Object.entries(templateConfig.fileStructure)
.map(([path, info]) => `${path} - ${info.description}`)
.join('\n')}
Conventions: Conventions:
${templateConfig.conventions.join("\n")} ${templateConfig.conventions.join('\n')}
Dependencies: Dependencies:
${JSON.stringify(templateConfig.dependencies, null, 2)} ${JSON.stringify(templateConfig.dependencies, null, 2)}
` : ''
Scripts:
${JSON.stringify(templateConfig.scripts, null, 2)}
`
: ""
// Create system message based on mode // Create system message based on mode
let systemMessage let systemMessage
@ -153,23 +100,13 @@ Instructions: ${messages[0].content}
Respond only with the modified code that can directly replace the existing code.` Respond only with the modified code that can directly replace the existing code.`
} else { } else {
systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. When providing code: systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example:
1. Format it using triple backticks (\`\`\`) with the appropriate language identifier. \`\`\`python
2. Always specify the complete file path in the format: print("Hello, World!")
${projectName}/filepath/to/file.ext \`\`\`
3. If creating a new file, specify the path as: Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
${projectName}/filepath/to/file.ext (new file)
4. Format your code blocks as:
${projectName}/filepath/to/file.ext
\`\`\`language
code here
\`\`\`
If multiple files are involved, repeat the format for each file. Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
This is the project template: This is the project template:
${templateContext} ${templateContext}
@ -209,10 +146,7 @@ ${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}`
new ReadableStream({ new ReadableStream({
async start(controller) { async start(controller) {
for await (const chunk of stream) { for await (const chunk of stream) {
if ( if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
controller.enqueue(encoder.encode(chunk.delta.text)) controller.enqueue(encoder.encode(chunk.delta.text))
} }
} }

View File

@ -1,69 +0,0 @@
import OpenAI from "openai"
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
export async function POST(request: Request) {
try {
const { originalCode, newCode, fileName } = await request.json()
const systemPrompt = `You are a code merging assistant. Your task is to merge the new code snippet with the original file content while:
1. Preserving the original file's functionality
2. Ensuring proper integration of the new code
3. Maintaining consistent style and formatting
4. Resolving any potential conflicts
5. Output ONLY the raw code without any:
- Code fence markers (\`\`\`)
- Language identifiers (typescript, javascript, etc.)
- Explanations or comments
- Markdown formatting
The output should be the exact code that will replace the existing code, nothing more and nothing less.
Important: When merging, preserve the original code structure as much as possible. Only make necessary changes to integrate the new code while maintaining the original code's organization and style.`
const mergedCode = `Original file (${fileName}):\n${originalCode}\n\nNew code to merge:\n${newCode}`
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: mergedCode },
],
prediction: {
type: "content",
content: mergedCode,
},
stream: true,
})
// Clean and stream response
const encoder = new TextEncoder()
return new Response(
new ReadableStream({
async start(controller) {
let buffer = ""
for await (const chunk of response) {
if (chunk.choices[0]?.delta?.content) {
buffer += chunk.choices[0].delta.content
// Clean any code fence markers that might appear in the stream
const cleanedContent = buffer
.replace(/^```[\w-]*\n|```\s*$/gm, "") // Remove code fences
.replace(/^(javascript|typescript|python|html|css)\n/gm, "") // Remove language identifiers
controller.enqueue(encoder.encode(cleanedContent))
buffer = ""
}
}
controller.close()
},
})
)
} catch (error) {
console.error("Merge error:", error)
return new Response(
error instanceof Error ? error.message : "Failed to merge code",
{ status: 500 }
)
}
}

BIN
frontend/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -102,27 +102,26 @@
.light .gradient-button-bg { .light .gradient-button-bg {
background: radial-gradient( background: radial-gradient(
circle at top, circle at top,
#f5f5f5 0%, #262626 0%,
/* Very light gray */ #e0e0e0 50% /* Soft gray */ #f5f5f5 50%
); ); /* Dark gray -> Light gray */
} }
.light .gradient-button { .light .gradient-button {
background: radial-gradient( background: radial-gradient(
circle at bottom, circle at bottom,
hsl(0, 0%, 85%) -10%, hsl(0, 10%, 25%) -10%,
/* Slightly darker gray */ hsl(0, 0%, 95%) 50% /* Very soft light gray */ #9d9d9d 50%
); ); /* Light gray -> Almost white */
} }
.light .gradient-button-bg > div:hover { .light .gradient-button-bg > div:hover {
background: radial-gradient( background: radial-gradient(
circle at bottom, circle at bottom,
hsl(0, 0%, 80%) -10%, hsl(0, 10%, 25%) -10%,
/* Slightly darker gray for hover */ hsl(0, 0%, 90%) 80% /* Softer gray */ #9d9d9d 80%
); ); /* Light gray -> Almost white */
} }
.inline-decoration::before { .inline-decoration::before {
content: "Generate"; content: "Generate";
color: #525252; color: #525252;
@ -176,23 +175,3 @@
.tab-scroll::-webkit-scrollbar { .tab-scroll::-webkit-scrollbar {
display: none; display: none;
} }
.added-line-decoration {
background-color: rgba(0, 255, 0, 0.1);
}
.removed-line-decoration {
background-color: rgba(255, 0, 0, 0.1);
}
.added-line-glyph {
background-color: #28a745;
width: 4px !important;
margin-left: 3px;
}
.removed-line-glyph {
background-color: #dc3545;
width: 4px !important;
margin-left: 3px;
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -11,22 +11,7 @@ import "./globals.css"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sandbox", title: "Sandbox",
description: description: "A collaborative, AI-powered cloud code editing environment",
"an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat",
openGraph: {
type: "website",
url: "https://sandbox.gitwit.dev",
title: "Sandbox",
description:
"an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat",
},
twitter: {
site: "https://sandbox.gitwit.dev",
title: "Sandbox by Gitwit",
description:
"an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat",
creator: "@gitwitdev",
},
} }
export default function RootLayout({ export default function RootLayout({

View File

@ -1 +0,0 @@
About Sandbox by Gitwit

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -8,20 +8,15 @@ import DashboardNavbarSearch from "./search"
export default function DashboardNavbar({ userData }: { userData: User }) { export default function DashboardNavbar({ userData }: { userData: User }) {
return ( return (
<div className=" py-2 px-4 w-full flex items-center justify-between border-b border-border"> <div className="h-16 px-4 w-full flex items-center justify-between border-b border-border">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-4">
<Link <Link
href="/" href="/"
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm" className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
> >
<Image src={Logo} alt="Logo" width={36} height={36} /> <Image src={Logo} alt="Logo" width={36} height={36} />
</Link> </Link>
<h1 className="text-xl"> <div className="text-sm font-medium flex items-center">Sandbox</div>
<span className="font-semibold">Sandbox</span>{" "}
<span className="text-xs font-medium text-muted-foreground">
by gitwit
</span>
</h1>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<DashboardNavbarSearch /> <DashboardNavbarSearch />

View File

@ -11,13 +11,13 @@ import {
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
export default function ProjectCardDropdown({ export default function ProjectCardDropdown({
visibility, sandbox,
onVisibilityChange, onVisibilityChange,
onDelete, onDelete,
}: { }: {
visibility: Sandbox["visibility"] sandbox: Sandbox
onVisibilityChange: () => void onVisibilityChange: (sandbox: Sandbox) => void
onDelete: () => void onDelete: (sandbox: Sandbox) => void
}) { }) {
return ( return (
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
@ -26,7 +26,7 @@ export default function ProjectCardDropdown({
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
}} }}
className="h-6 w-6 z-10 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground" className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
> >
<Ellipsis className="w-4 h-4" /> <Ellipsis className="w-4 h-4" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -34,11 +34,11 @@ export default function ProjectCardDropdown({
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onVisibilityChange() onVisibilityChange(sandbox)
}} }}
className="cursor-pointer" className="cursor-pointer"
> >
{visibility === "public" ? ( {sandbox.visibility === "public" ? (
<> <>
<Lock className="mr-2 h-4 w-4" /> <Lock className="mr-2 h-4 w-4" />
<span>Make Private</span> <span>Make Private</span>
@ -53,7 +53,7 @@ export default function ProjectCardDropdown({
<DropdownMenuItem <DropdownMenuItem
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onDelete() onDelete(sandbox)
}} }}
className="!text-destructive cursor-pointer" className="!text-destructive cursor-pointer"
> >

View File

@ -1,235 +1,59 @@
"use client" "use client"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card" import { Card } from "@/components/ui/card"
import { toggleLike } from "@/lib/actions"
import { projectTemplates } from "@/lib/data" import { projectTemplates } from "@/lib/data"
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types"
import { cn } from "@/lib/utils"
import { useUser } from "@clerk/nextjs"
import { AnimatePresence, motion } from "framer-motion" import { AnimatePresence, motion } from "framer-motion"
import { Clock, Eye, Globe, Heart, Lock } from "lucide-react" import { Clock, Globe, Lock } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import Link from "next/link"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { import { useEffect, useState } from "react"
memo,
MouseEventHandler,
useEffect,
useMemo,
useOptimistic,
useState,
useTransition,
} from "react"
import ProjectCardDropdown from "./dropdown" import ProjectCardDropdown from "./dropdown"
import { CanvasRevealEffect } from "./revealEffect"
type BaseProjectCardProps = { export default function ProjectCard({
id: string children,
name: string sandbox,
type: string onVisibilityChange,
visibility: "public" | "private" onDelete,
createdAt: Date deletingId,
likeCount: number }: {
liked?: boolean children?: React.ReactNode
viewCount: number sandbox: Sandbox
} onVisibilityChange: (sandbox: Sandbox) => void
onDelete: (sandbox: Sandbox) => void
type AuthenticatedProjectCardProps = BaseProjectCardProps & {
isAuthenticated: true
onVisibilityChange: (
sandbox: Pick<Sandbox, "id" | "name" | "visibility">
) => void
onDelete: (sandbox: Pick<Sandbox, "id" | "name">) => void
deletingId: string deletingId: string
} }) {
type UnauthenticatedProjectCardProps = BaseProjectCardProps & {
isAuthenticated: false
}
type ProjectCardProps =
| AuthenticatedProjectCardProps
| UnauthenticatedProjectCardProps
const StatItem = memo(({ icon: Icon, value }: { icon: any; value: number }) => (
<div className="flex items-center space-x-1">
<Icon className="size-4" />
<span className="text-xs">{value}</span>
</div>
))
StatItem.displayName = "StatItem"
const formatDate = (date: Date): string => {
const now = new Date()
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / 60000)
if (diffInMinutes < 1) return "Now"
if (diffInMinutes < 60) return `${diffInMinutes}m ago`
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`
return `${Math.floor(diffInMinutes / 1440)}d ago`
}
const ProjectMetadata = memo(
({
id,
visibility,
createdAt,
likeCount,
liked,
viewCount,
}: Pick<
BaseProjectCardProps,
"visibility" | "createdAt" | "likeCount" | "liked" | "viewCount" | "id"
>) => {
const { user } = useUser()
const [date, setDate] = useState<string>()
const Icon = visibility === "private" ? Lock : Globe
useEffect(() => {
setDate(formatDate(new Date(createdAt)))
}, [createdAt])
return (
<div className="flex flex-col text-muted-foreground space-y-2 text-sm z-10">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Icon className="size-4" />
<span className="text-xs">
{visibility === "private" ? "Private" : "Public"}
</span>
</div>
</div>
<div className="flex gap-3">
<div className="flex items-center gap-2">
<Clock className="size-4" /> <span className="text-xs">{date}</span>
</div>
<LikeButton
sandboxId={id}
initialIsLiked={!!liked}
initialLikeCount={likeCount}
userId={user?.id ?? null}
/>
<StatItem icon={Eye} value={viewCount} />
</div>
</div>
)
}
)
ProjectMetadata.displayName = "ProjectMetadata"
interface LikeButtonProps {
sandboxId: string
userId: string | null
initialLikeCount: number
initialIsLiked: boolean
}
export function LikeButton({
sandboxId,
userId,
initialLikeCount,
initialIsLiked,
}: LikeButtonProps) {
// Optimistic state for like status and count
const [{ isLiked, likeCount }, optimisticUpdateLike] = useOptimistic(
{ isLiked: initialIsLiked, likeCount: initialLikeCount },
(state, optimisticValue: boolean) => {
return {
isLiked: optimisticValue,
likeCount: state.likeCount + (optimisticValue ? 1 : -1),
}
}
)
const [isPending, startTransition] = useTransition()
const handleLike: MouseEventHandler<HTMLButtonElement> = async (e) => {
e.stopPropagation() // Prevent click event from bubbling up which leads to navigation to /code/:id
if (!userId) return
startTransition(async () => {
const newLikeState = !isLiked
try {
optimisticUpdateLike(newLikeState)
await toggleLike(sandboxId, userId)
} catch (error) {
console.log("error", error)
optimisticUpdateLike(!newLikeState)
}
})
}
return (
<Button
variant="ghost"
size="sm"
disabled={!userId || isPending}
onClick={handleLike}
className="gap-1 px-1 rounded-full"
>
<Heart
className={cn("size-4", isLiked ? "stroke-red-500 fill-red-500" : "")}
/>
<span className="text-xs">{likeCount}</span>
</Button>
)
}
function ProjectCardComponent({
id,
name,
type,
visibility,
createdAt,
likeCount,
viewCount,
...props
}: ProjectCardProps) {
const [hovered, setHovered] = useState(false) const [hovered, setHovered] = useState(false)
const [date, setDate] = useState<string>()
const router = useRouter() const router = useRouter()
const projectIcon = useMemo( useEffect(() => {
() => const createdAt = new Date(sandbox.createdAt)
projectTemplates.find((p) => p.id === type)?.icon ?? const now = new Date()
"/project-icons/node.svg", const diffInMinutes = Math.floor(
[type] (now.getTime() - createdAt.getTime()) / 60000
) )
const handleVisibilityChange = () => { if (diffInMinutes < 1) {
if (props.isAuthenticated) { setDate("Now")
props.onVisibilityChange({ } else if (diffInMinutes < 60) {
id, setDate(`${diffInMinutes}m ago`)
name, } else if (diffInMinutes < 1440) {
visibility, setDate(`${Math.floor(diffInMinutes / 60)}h ago`)
}) } else {
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
} }
} }, [sandbox])
const projectIcon =
const handleDelete = () => { projectTemplates.find((p) => p.id === sandbox.type)?.icon ??
if (props.isAuthenticated) { "/project-icons/node.svg"
props.onDelete({
id,
name,
})
}
}
return ( return (
<Card <Card
tabIndex={0} tabIndex={0}
onClick={() => router.push(`/code/${id}`)} onClick={() => router.push(`/code/${sandbox.id}`)}
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
className={` className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`}
group/canvas-card p-4 h-48 flex flex-col justify-between items-start
hover:border-muted-foreground/50 relative overflow-hidden transition-all
${
props.isAuthenticated && props.deletingId === id
? "opacity-50 pointer-events-none cursor-events-none"
: "cursor-pointer"
}
`}
> >
<AnimatePresence> <AnimatePresence>
{hovered && ( {hovered && (
@ -238,64 +62,38 @@ function ProjectCardComponent({
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
className="h-full w-full absolute inset-0" className="h-full w-full absolute inset-0"
> >
<CanvasRevealEffect {children}
animationSpeed={3}
containerClassName="bg-muted"
colors={colors[type]}
dotSize={2}
/>
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
<div className="space-x-2 flex items-center justify-start w-full z-10"> <div className="space-x-2 flex items-center justify-start w-full z-10">
<Image <Image alt="" src={projectIcon} width={20} height={20} />
alt={`${type} project icon`} <div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
src={projectIcon} {sandbox.name}
width={20} </div>
height={20} <ProjectCardDropdown
sandbox={sandbox}
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
/> />
<Link
href={`/code/${id}`}
className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden before:content-[''] before:absolute before:z-0 before:top-0 before:left-0 before:w-full before:h-full before:rounded-xl"
>
{name}
</Link>
{props.isAuthenticated && (
<ProjectCardDropdown
onVisibilityChange={handleVisibilityChange}
onDelete={handleDelete}
visibility={visibility}
/>
)}
</div> </div>
<div className="flex flex-col text-muted-foreground space-y-0.5 text-sm z-10">
<ProjectMetadata <div className="flex items-center">
visibility={visibility} {sandbox.visibility === "private" ? (
createdAt={createdAt} <>
likeCount={likeCount} <Lock className="w-3 h-3 mr-2" /> Private
viewCount={viewCount} </>
id={id} ) : (
liked={props.liked} <>
/> <Globe className="w-3 h-3 mr-2" /> Public
</>
)}
</div>
<div className="flex items-center">
<Clock className="w-3 h-3 mr-2" /> {date}
</div>
</div>
</Card> </Card>
) )
} }
ProjectCardComponent.displayName = "ProjectCard"
const ProjectCard = memo(ProjectCardComponent)
export default ProjectCard
const colors: { [key: string]: number[][] } = {
react: [
[71, 207, 237],
[30, 126, 148],
],
node: [
[86, 184, 72],
[59, 112, 52],
],
}

View File

@ -2,9 +2,11 @@
import { deleteSandbox, updateSandbox } from "@/lib/actions" import { deleteSandbox, updateSandbox } from "@/lib/actions"
import { Sandbox } from "@/lib/types" import { Sandbox } from "@/lib/types"
import { useEffect, useMemo, useState } from "react" import Link from "next/link"
import { useEffect, useState } from "react"
import { toast } from "sonner" import { toast } from "sonner"
import ProjectCard from "./projectCard" import ProjectCard from "./projectCard"
import { CanvasRevealEffect } from "./projectCard/revealEffect"
const colors: { [key: string]: number[][] } = { const colors: { [key: string]: number[][] } = {
react: [ react: [
@ -26,27 +28,11 @@ export default function DashboardProjects({
}) { }) {
const [deletingId, setDeletingId] = useState<string>("") const [deletingId, setDeletingId] = useState<string>("")
const onVisibilityChange = useMemo( const onDelete = async (sandbox: Sandbox) => {
() => async (sandbox: Pick<Sandbox, "id" | "name" | "visibility">) => { setDeletingId(sandbox.id)
const newVisibility = toast(`Project ${sandbox.name} deleted.`)
sandbox.visibility === "public" ? "private" : "public" await deleteSandbox(sandbox.id)
toast(`Project ${sandbox.name} is now ${newVisibility}.`) }
await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
})
},
[]
)
const onDelete = useMemo(
() => async (sandbox: Pick<Sandbox, "id" | "name">) => {
setDeletingId(sandbox.id)
toast(`Project ${sandbox.name} deleted.`)
await deleteSandbox(sandbox.id)
},
[]
)
useEffect(() => { useEffect(() => {
if (deletingId) { if (deletingId) {
@ -54,6 +40,15 @@ export default function DashboardProjects({
} }
}, [sandboxes]) }, [sandboxes])
const onVisibilityChange = async (sandbox: Sandbox) => {
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
})
}
return ( return (
<div className="grow p-4 flex flex-col"> <div className="grow p-4 flex flex-col">
<div className="text-xl font-medium mb-8"> <div className="text-xl font-medium mb-8">
@ -69,14 +64,30 @@ export default function DashboardProjects({
} }
} }
return ( return (
<ProjectCard <Link
key={sandbox.id} key={sandbox.id}
onVisibilityChange={onVisibilityChange} href={`/code/${sandbox.id}`}
onDelete={onDelete} className={`${
deletingId={deletingId} deletingId === sandbox.id
isAuthenticated ? "pointer-events-none opacity-50 cursor-events-none"
{...sandbox} : "cursor-pointer"
/> } transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg`}
>
<ProjectCard
sandbox={sandbox}
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
deletingId={deletingId}
>
<CanvasRevealEffect
animationSpeed={3}
containerClassName="bg-black"
colors={colors[sandbox.type]}
dotSize={2}
/>
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</ProjectCard>
</Link>
) )
})} })}
</div> </div>

View File

@ -1,77 +0,0 @@
import { Check, Loader2 } from "lucide-react"
import { useState } from "react"
import { toast } from "sonner"
import { Button } from "../../ui/button"
interface ApplyButtonProps {
code: string
activeFileName: string
activeFileContent: string
editorRef: { current: any }
onApply: (mergedCode: string, originalCode: string) => void
}
export default function ApplyButton({
code,
activeFileName,
activeFileContent,
editorRef,
onApply,
}: ApplyButtonProps) {
const [isApplying, setIsApplying] = useState(false)
const handleApply = async () => {
setIsApplying(true)
try {
const response = await fetch("/api/merge", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
originalCode: activeFileContent,
newCode: String(code),
fileName: activeFileName,
}),
})
if (!response.ok) {
throw new Error(await response.text())
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let mergedCode = ""
if (reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
mergedCode += decoder.decode(value, { stream: true })
}
}
onApply(mergedCode.trim(), activeFileContent)
} catch (error) {
console.error("Error applying code:", error)
toast.error(
error instanceof Error ? error.message : "Failed to apply code changes"
)
} finally {
setIsApplying(false)
}
}
return (
<Button
onClick={handleApply}
size="sm"
variant="ghost"
className="p-1 h-6"
disabled={isApplying}
>
{isApplying ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Check className="w-4 h-4" />
)}
</Button>
)
}

View File

@ -13,13 +13,6 @@ export default function ChatMessage({
setContext, setContext,
setIsContextExpanded, setIsContextExpanded,
socket, socket,
handleApplyCode,
activeFileName,
activeFileContent,
editorRef,
mergeDecorationsCollection,
setMergeDecorationsCollection,
selectFile,
}: MessageProps) { }: MessageProps) {
// State for expanded message index // State for expanded message index
const [expandedMessageIndex, setExpandedMessageIndex] = useState< const [expandedMessageIndex, setExpandedMessageIndex] = useState<
@ -111,14 +104,7 @@ export default function ChatMessage({
const components = createMarkdownComponents( const components = createMarkdownComponents(
renderCopyButton, renderCopyButton,
renderMarkdownElement, renderMarkdownElement,
askAboutCode, askAboutCode
activeFileName,
activeFileContent,
editorRef,
handleApplyCode,
selectFile,
mergeDecorationsCollection,
setMergeDecorationsCollection,
) )
return ( return (
@ -126,8 +112,8 @@ export default function ChatMessage({
<div <div
className={`relative p-2 rounded-lg ${ className={`relative p-2 rounded-lg ${
message.role === "user" message.role === "user"
? "bg-foreground text-background" ? "bg-[#262626] text-white"
: "bg-background text-foreground" : "bg-transparent text-white"
} max-w-full`} } max-w-full`}
> >
{/* Render context tabs */} {/* Render context tabs */}
@ -171,7 +157,7 @@ export default function ChatMessage({
end: e.target.value.split("\n").length, end: e.target.value.split("\n").length,
}) })
}} }}
className="w-full p-2 bg-[#1e1e1e] text-foreground font-mono text-sm rounded" className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
rows={code.split("\n").length} rows={code.split("\n").length}
style={{ style={{
resize: "vertical", resize: "vertical",
@ -215,8 +201,7 @@ export default function ChatMessage({
// Parse context to tabs for context tabs component // Parse context to tabs for context tabs component
function parseContextToTabs(context: string) { function parseContextToTabs(context: string) {
// Use specific regex patterns to avoid matching import statements const sections = context.split(/(?=File |Code from )/)
const sections = context.split(/(?=File |Code from |Image \d{1,2}:)/)
return sections return sections
.map((section, index) => { .map((section, index) => {
const lines = section.trim().split("\n") const lines = section.trim().split("\n")
@ -226,29 +211,16 @@ function parseContextToTabs(context: string) {
// Remove code block markers for display // Remove code block markers for display
content = content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "") content = content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
// Determine the type of context // Determine if the context is a file or code
const isFile = titleLine.startsWith("File ") const isFile = titleLine.startsWith("File ")
const isImage = titleLine.startsWith("Image ") const name = titleLine.replace(/^(File |Code from )/, "").replace(":", "")
const type = isFile ? "file" : isImage ? "image" : "code"
const name = titleLine
.replace(/^(File |Code from |Image )/, "")
.replace(":", "")
.trim()
// Skip if the content is empty or if it's just an import statement
if (!content || content.trim().startsWith('from "')) {
return null
}
return { return {
id: `context-${index}`, id: `context-${index}`,
type: type as "file" | "code" | "image", type: isFile ? ("file" as const) : ("code" as const),
name: name, name: name,
content: content, content: content,
} }
}) })
.filter( .filter((tab) => tab.content.length > 0)
(tab): tab is NonNullable<typeof tab> =>
tab !== null && tab.content.length > 0
)
} }

View File

@ -1,7 +1,6 @@
import { ScrollArea } from "@/components/ui/scroll-area"
import { useSocket } from "@/context/SocketContext" import { useSocket } from "@/context/SocketContext"
import { TFile } from "@/lib/types" import { TFile } from "@/lib/types"
import { ChevronDown, X } from "lucide-react" import { X, ChevronDown } from "lucide-react"
import { nanoid } from "nanoid" import { nanoid } from "nanoid"
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import LoadingDots from "../../ui/LoadingDots" import LoadingDots from "../../ui/LoadingDots"
@ -19,11 +18,6 @@ export default function AIChat({
lastCopiedRangeRef, lastCopiedRangeRef,
files, files,
templateType, templateType,
handleApplyCode,
selectFile,
mergeDecorationsCollection,
setMergeDecorationsCollection,
projectName,
}: AIChatProps) { }: AIChatProps) {
// Initialize socket and messages // Initialize socket and messages
const { socket } = useSocket() const { socket } = useSocket()
@ -81,8 +75,8 @@ export default function AIChat({
useEffect(() => { useEffect(() => {
const container = chatContainerRef.current const container = chatContainerRef.current
if (container) { if (container) {
container.addEventListener("scroll", handleScroll) container.addEventListener('scroll', handleScroll)
return () => container.removeEventListener("scroll", handleScroll) return () => container.removeEventListener('scroll', handleScroll)
} }
}, []) }, [])
@ -131,8 +125,6 @@ export default function AIChat({
} else if (tab.type === "code") { } else if (tab.type === "code") {
const cleanContent = formatCodeContent(tab.content) const cleanContent = formatCodeContent(tab.content)
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\`` return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
} else if (tab.type === "image") {
return `Image ${tab.name}:\n${tab.content}`
} }
return `${tab.name}:\n${tab.content}` return `${tab.name}:\n${tab.content}`
}) })
@ -154,10 +146,10 @@ export default function AIChat({
abortControllerRef, abortControllerRef,
activeFileContent, activeFileContent,
false, false,
templateType, templateType
files,
projectName
) )
// Clear context tabs after sending
setContextTabs([])
} }
// Set context for the chat // Set context for the chat
@ -175,22 +167,6 @@ export default function AIChat({
addContextTab("code", name, context, range) addContextTab("code", name, context, range)
} }
// update context tabs when file contents change
useEffect(() => {
setContextTabs((prevTabs) =>
prevTabs.map((tab) => {
if (tab.type === "file" && tab.name === activeFileName) {
const fileExt = tab.name.split(".").pop() || "txt"
return {
...tab,
content: `\`\`\`${fileExt}\n${activeFileContent}\n\`\`\``,
}
}
return tab
})
)
}, [activeFileContent, activeFileName])
return ( return (
<div className="flex flex-col h-screen w-full"> <div className="flex flex-col h-screen w-full">
<div className="flex justify-between items-center p-2 border-b"> <div className="flex justify-between items-center p-2 border-b">
@ -209,9 +185,9 @@ export default function AIChat({
</button> </button>
</div> </div>
</div> </div>
<ScrollArea <div
ref={chatContainerRef} ref={chatContainerRef}
className="flex-grow p-4 space-y-4 relative" className="flex-grow overflow-y-auto p-4 space-y-4 relative"
> >
{messages.map((message, messageIndex) => ( {messages.map((message, messageIndex) => (
// Render chat message component for each message // Render chat message component for each message
@ -221,13 +197,6 @@ export default function AIChat({
setContext={setContext} setContext={setContext}
setIsContextExpanded={setIsContextExpanded} setIsContextExpanded={setIsContextExpanded}
socket={socket} socket={socket}
handleApplyCode={handleApplyCode}
activeFileName={activeFileName}
activeFileContent={activeFileContent}
editorRef={editorRef}
mergeDecorationsCollection={mergeDecorationsCollection}
setMergeDecorationsCollection={setMergeDecorationsCollection}
selectFile={selectFile}
/> />
))} ))}
{isLoading && <LoadingDots />} {isLoading && <LoadingDots />}
@ -242,7 +211,7 @@ export default function AIChat({
<ChevronDown className="h-5 w-5" /> <ChevronDown className="h-5 w-5" />
</button> </button>
)} )}
</ScrollArea> </div>
<div className="p-4 border-t mb-14"> <div className="p-4 border-t mb-14">
{/* Render context tabs component */} {/* Render context tabs component */}
<ContextTabs <ContextTabs

View File

@ -1,4 +1,3 @@
import { TFile, TFolder } from "@/lib/types"
import React from "react" import React from "react"
// Stringify content for chat message component // Stringify content for chat message component
@ -92,9 +91,7 @@ export const handleSend = async (
abortControllerRef: React.MutableRefObject<AbortController | null>, abortControllerRef: React.MutableRefObject<AbortController | null>,
activeFileContent: string, activeFileContent: string,
isEditMode: boolean = false, isEditMode: boolean = false,
templateType: string, templateType: string
files: (TFile | TFolder)[],
projectName: string
) => { ) => {
// Return if input is empty and context is null // Return if input is empty and context is null
if (input.trim() === "" && !context) return if (input.trim() === "" && !context) return
@ -134,22 +131,22 @@ export const handleSend = async (
})) }))
// Fetch AI response for chat message component // Fetch AI response for chat message component
const response = await fetch("/api/ai", { const response = await fetch("/api/ai",
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
messages: anthropicMessages, messages: anthropicMessages,
context: context || undefined, context: context || undefined,
activeFileContent: activeFileContent, activeFileContent: activeFileContent,
isEditMode: isEditMode, isEditMode: isEditMode,
templateType: templateType, templateType: templateType,
files: files, }),
projectName: projectName, signal: abortControllerRef.current.signal,
}), }
signal: abortControllerRef.current.signal, )
})
// Throw error if response is not ok // Throw error if response is not ok
if (!response.ok) { if (!response.ok) {
@ -204,8 +201,7 @@ export const handleSend = async (
console.error("Error fetching AI response:", error) console.error("Error fetching AI response:", error)
const errorMessage = { const errorMessage = {
role: "assistant" as const, role: "assistant" as const,
content: content: error.message || "Sorry, I encountered an error. Please try again.",
error.message || "Sorry, I encountered an error. Please try again.",
} }
setMessages((prev) => [...prev, errorMessage]) setMessages((prev) => [...prev, errorMessage])
} }
@ -243,11 +239,3 @@ export const looksLikeCode = (text: string): boolean => {
return codeIndicators.some((pattern) => pattern.test(text)) return codeIndicators.some((pattern) => pattern.test(text))
} }
// Add this new function after looksLikeCode function
export const isFilePath = (text: string): boolean => {
// Match patterns like next/styles/SignIn.module.css or path/to/file.ext (new file)
const pattern =
/^(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+(\s+\(new file\))?$/
return pattern.test(text)
}

View File

@ -1,26 +1,15 @@
import { useSocket } from "@/context/SocketContext" import { CornerUpLeft } from "lucide-react"
import { TTab } from "@/lib/types"
import { Check, CornerUpLeft, FileText, X } from "lucide-react"
import monaco from "monaco-editor"
import { Components } from "react-markdown" import { Components } from "react-markdown"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism" import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
import { Button } from "../../../ui/button" import { Button } from "../../../ui/button"
import ApplyButton from "../ApplyButton" import { stringifyContent } from "./chatUtils"
import { isFilePath, stringifyContent } from "./chatUtils"
// Create markdown components for chat message component // Create markdown components for chat message component
export const createMarkdownComponents = ( export const createMarkdownComponents = (
renderCopyButton: (text: any) => JSX.Element, renderCopyButton: (text: any) => JSX.Element,
renderMarkdownElement: (props: any) => JSX.Element, renderMarkdownElement: (props: any) => JSX.Element,
askAboutCode: (code: any) => void, askAboutCode: (code: any) => void
activeFileName: string,
activeFileContent: string,
editorRef: any,
handleApplyCode: (mergedCode: string, originalCode: string) => void,
selectFile: (tab: TTab) => void,
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection,
setMergeDecorationsCollection?: (collection: undefined) => void
): Components => ({ ): Components => ({
code: ({ code: ({
node, node,
@ -36,92 +25,39 @@ export const createMarkdownComponents = (
const match = /language-(\w+)/.exec(className || "") const match = /language-(\w+)/.exec(className || "")
return match ? ( return match ? (
<div className="relative border border-input rounded-md mt-8 my-2 translate-y-[-1rem]"> <div className="relative border border-input rounded-md my-4">
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 rounded-tl"> <div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
{match[1]} {match[1]}
</div> </div>
<div className="sticky top-0 right-0 flex justify-end z-10"> <div className="absolute top-0 right-0 flex">
<div className="flex border border-input shadow-lg bg-background rounded-md"> {renderCopyButton(children)}
{renderCopyButton(children)} <Button
<div className="w-px bg-input"></div> onClick={(e) => {
{!mergeDecorationsCollection ? ( e.preventDefault()
<ApplyButton e.stopPropagation()
code={String(children)} askAboutCode(children)
activeFileName={activeFileName} }}
activeFileContent={activeFileContent} size="sm"
editorRef={editorRef} variant="ghost"
onApply={handleApplyCode} className="p-1 h-6"
/> >
) : ( <CornerUpLeft className="w-4 h-4" />
<> </Button>
<Button </div>
onClick={() => { <div className="pt-6">
if ( <SyntaxHighlighter
setMergeDecorationsCollection && style={vscDarkPlus as any}
mergeDecorationsCollection && language={match[1]}
editorRef?.current PreTag="div"
) { customStyle={{
mergeDecorationsCollection?.clear() margin: 0,
setMergeDecorationsCollection(undefined) padding: "0.5rem",
} fontSize: "0.875rem",
}} }}
size="sm" >
variant="ghost" {stringifyContent(children)}
className="p-1 h-6" </SyntaxHighlighter>
title="Accept Changes"
>
<Check className="w-4 h-4 text-green-500" />
</Button>
<div className="w-px bg-input"></div>
<Button
onClick={() => {
if (editorRef?.current && mergeDecorationsCollection) {
const model = editorRef.current.getModel()
if (model && (model as any).originalContent) {
editorRef.current?.setValue(
(model as any).originalContent
)
mergeDecorationsCollection.clear()
setMergeDecorationsCollection?.(undefined)
}
}
}}
size="sm"
variant="ghost"
className="p-1 h-6"
title="Discard Changes"
>
<X className="w-4 h-4 text-red-500" />
</Button>
</>
)}
<div className="w-px bg-input"></div>
<Button
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
askAboutCode(children)
}}
size="sm"
variant="ghost"
className="p-1 h-6"
>
<CornerUpLeft className="w-4 h-4" />
</Button>
</div>
</div> </div>
<SyntaxHighlighter
style={vscDarkPlus as any}
language={match[1]}
PreTag="div"
customStyle={{
margin: 0,
padding: "0.5rem",
fontSize: "0.875rem",
}}
>
{stringifyContent(children)}
</SyntaxHighlighter>
</div> </div>
) : ( ) : (
<code className={className} {...props}> <code className={className} {...props}>
@ -130,62 +66,8 @@ export const createMarkdownComponents = (
) )
}, },
// Render markdown elements // Render markdown elements
p: ({ node, children, ...props }) => { p: ({ node, children, ...props }) =>
const content = stringifyContent(children) renderMarkdownElement({ node, children, ...props }),
const { socket } = useSocket()
if (isFilePath(content)) {
const isNewFile = content.endsWith("(new file)")
const filePath = (
isNewFile ? content.replace(" (new file)", "") : content
)
.split("/")
.filter((part, index) => index !== 0)
.join("/")
const handleFileClick = () => {
if (isNewFile) {
socket?.emit(
"createFile",
{
name: filePath,
},
(response: any) => {
if (response.success) {
const tab: TTab = {
id: filePath,
name: filePath.split("/").pop() || "",
saved: true,
type: "file",
}
selectFile(tab)
}
}
)
} else {
const tab: TTab = {
id: filePath,
name: filePath.split("/").pop() || "",
saved: true,
type: "file",
}
selectFile(tab)
}
}
return (
<div
onClick={handleFileClick}
className="group flex items-center gap-2 px-2 py-1 bg-secondary/50 rounded-md my-2 text-xs hover:bg-secondary cursor-pointer w-fit"
>
<FileText className="h-4 w-4" />
<span className="font-mono group-hover:underline">{content}</span>
</div>
)
}
return renderMarkdownElement({ node, children, ...props })
},
h1: ({ node, children, ...props }) => h1: ({ node, children, ...props }) =>
renderMarkdownElement({ node, children, ...props }), renderMarkdownElement({ node, children, ...props }),
h2: ({ node, children, ...props }) => h2: ({ node, children, ...props }) =>

View File

@ -1,5 +1,5 @@
import { TemplateConfig } from "@/lib/templates" import { TemplateConfig } from "@/lib/templates"
import { TFile, TFolder, TTab } from "@/lib/types" import { TFile, TFolder } from "@/lib/types"
import * as monaco from "monaco-editor" import * as monaco from "monaco-editor"
import { Socket } from "socket.io-client" import { Socket } from "socket.io-client"
@ -56,11 +56,6 @@ export interface AIChatProps {
files: (TFile | TFolder)[] files: (TFile | TFolder)[]
templateType: string templateType: string
templateConfig?: TemplateConfig templateConfig?: TemplateConfig
projectName: string
handleApplyCode: (mergedCode: string, originalCode: string) => void
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
setMergeDecorationsCollection?: (collection: undefined) => void
selectFile: (tab: TTab) => void
} }
// Chat input props interface // Chat input props interface
@ -110,13 +105,6 @@ export interface MessageProps {
) => void ) => void
setIsContextExpanded: (isExpanded: boolean) => void setIsContextExpanded: (isExpanded: boolean) => void
socket: Socket | null socket: Socket | null
handleApplyCode: (mergedCode: string, originalCode: string) => void
activeFileName: string
activeFileContent: string
editorRef: any
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
setMergeDecorationsCollection?: (collection: undefined) => void
selectFile: (tab: TTab) => void
} }
// Context tabs props interface // Context tabs props interface

View File

@ -66,17 +66,15 @@ export default function GenerateInput({
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
messages: [ messages: [{
{ role: "user",
role: "user", content: regenerate ? currentPrompt : input
content: regenerate ? currentPrompt : input, }],
},
],
context: null, context: null,
activeFileContent: data.code, activeFileContent: data.code,
isEditMode: true, isEditMode: true,
fileName: data.fileName, fileName: data.fileName,
line: data.line, line: data.line
}), }),
}) })

View File

@ -62,7 +62,7 @@ export default function CodeEditor({
//SocketContext functions and effects //SocketContext functions and effects
const { socket, setUserAndSandboxId } = useSocket() const { socket, setUserAndSandboxId } = useSocket()
// theme // theme
const { resolvedTheme: theme } = useTheme() const { theme } = useTheme()
useEffect(() => { useEffect(() => {
// Ensure userData.id and sandboxData.id are available before attempting to connect // Ensure userData.id and sandboxData.id are available before attempting to connect
if (userData.id && sandboxData.id) { if (userData.id && sandboxData.id) {
@ -104,13 +104,6 @@ export default function CodeEditor({
// Added this state to track the most recent content for each file // Added this state to track the most recent content for each file
const [fileContents, setFileContents] = useState<Record<string, string>>({}) const [fileContents, setFileContents] = useState<Record<string, string>>({})
// Apply Button merger decoration state
const [mergeDecorations, setMergeDecorations] = useState<
monaco.editor.IModelDeltaDecoration[]
>([])
const [mergeDecorationsCollection, setMergeDecorationsCollection] =
useState<monaco.editor.IEditorDecorationsCollection>()
// Editor state // Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext") const [editorLanguage, setEditorLanguage] = useState("plaintext")
const [cursorLine, setCursorLine] = useState(0) const [cursorLine, setCursorLine] = useState(0)
@ -382,57 +375,6 @@ export default function CodeEditor({
}) })
}, [editorRef]) }, [editorRef])
// handle apply code
const handleApplyCode = useCallback(
(mergedCode: string, originalCode: string) => {
if (!editorRef) return
const model = editorRef.getModel()
if (!model) return // Store original content
;(model as any).originalContent = originalCode
// Calculate the full range of the document
const fullRange = model.getFullModelRange()
// Create decorations before applying the edit
const originalLines = originalCode.split("\n")
const mergedLines = mergedCode.split("\n")
const decorations: monaco.editor.IModelDeltaDecoration[] = []
for (
let i = 0;
i < Math.max(originalLines.length, mergedLines.length);
i++
) {
// Only highlight new lines (green highlights)
if (i >= originalLines.length || originalLines[i] !== mergedLines[i]) {
decorations.push({
range: new monaco.Range(i + 1, 1, i + 1, 1),
options: {
isWholeLine: true,
className: "added-line-decoration",
glyphMarginClassName: "added-line-glyph",
},
})
}
}
// Execute the edit operation
editorRef.executeEdits("apply-code", [
{
range: fullRange,
text: mergedCode,
forceMoveMarkers: true,
},
])
// Apply decorations after the edit
const newDecorations = editorRef.createDecorationsCollection(decorations)
setMergeDecorationsCollection(newDecorations)
},
[editorRef]
)
// Generate widget effect // Generate widget effect
useEffect(() => { useEffect(() => {
if (generate.show) { if (generate.show) {
@ -793,32 +735,31 @@ export default function CodeEditor({
setGenerate((prev) => ({ ...prev, show: false })) setGenerate((prev) => ({ ...prev, show: false }))
// Check if the tab already exists in the list of open tabs // Check if the tab already exists in the list of open tabs
const existingTab = tabs.find((t) => t.id === tab.id) const exists = tabs.find((t) => t.id === tab.id)
setTabs((prev) => {
if (existingTab) { if (exists) {
// If the tab exists, just make it active // If the tab exists, make it the active tab
setActiveFileId(existingTab.id) setActiveFileId(exists.id)
if (fileContents[existingTab.id]) { return prev
setActiveFileContent(fileContents[existingTab.id])
} }
// If the tab doesn't exist, add it to the list of tabs and make it active
return [...prev, tab]
})
// If the file's content is already cached, set it as the active content
if (fileContents[tab.id]) {
setActiveFileContent(fileContents[tab.id])
} else { } else {
// If the tab doesn't exist, add it to the list and make it active // Otherwise, fetch the content of the file and cache it
setTabs((prev) => [...prev, tab]) debouncedGetFile(tab.id, (response: string) => {
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
// Fetch content if not cached setActiveFileContent(response)
if (!fileContents[tab.id]) { })
debouncedGetFile(tab.id, (response: string) => {
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
setActiveFileContent(response)
})
} else {
setActiveFileContent(fileContents[tab.id])
}
} }
// Set the editor language based on the file type // Set the editor language based on the file type
setEditorLanguage(processFileType(tab.name)) setEditorLanguage(processFileType(tab.name))
// Set the active file ID // Set the active file ID to the new tab
setActiveFileId(tab.id) setActiveFileId(tab.id)
} }
@ -980,9 +921,9 @@ export default function CodeEditor({
) )
return ( return (
<div className="flex max-h-full overflow-hidden"> <>
{/* Copilot DOM elements */}
<PreviewProvider> <PreviewProvider>
{/* Copilot DOM elements */}
<div ref={generateRef} /> <div ref={generateRef} />
<div ref={suggestionRef} className="absolute"> <div ref={suggestionRef} className="absolute">
<AnimatePresence> <AnimatePresence>
@ -1146,62 +1087,62 @@ export default function CodeEditor({
</div> </div>
</> </>
) : // 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}
beforeMount={handleEditorWillMount} beforeMount={handleEditorWillMount}
onMount={handleEditorMount} onMount={handleEditorMount}
onChange={(value) => { onChange={(value) => {
// If the new content is different from the cached content, update it // If the new content is different from the cached content, update it
if (value !== fileContents[activeFileId]) { if (value !== fileContents[activeFileId]) {
setActiveFileContent(value ?? "") // Update the active file content setActiveFileContent(value ?? "") // Update the active file content
// Mark the file as unsaved by setting 'saved' to false // Mark the file as unsaved by setting 'saved' to false
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => prev.map((tab) =>
tab.id === activeFileId tab.id === activeFileId
? { ...tab, saved: false } ? { ...tab, saved: false }
: tab : tab
)
) )
) } else {
} else { // If the content matches the cached content, mark the file as saved
// If the content matches the cached content, mark the file as saved setTabs((prev) =>
setTabs((prev) => prev.map((tab) =>
prev.map((tab) => tab.id === activeFileId
tab.id === activeFileId ? { ...tab, saved: true }
? { ...tab, saved: true } : tab
: tab )
) )
) }
} }}
}} options={{
options={{ tabSize: 2,
tabSize: 2, minimap: {
minimap: { enabled: false,
enabled: false, },
}, padding: {
padding: { bottom: 4,
bottom: 4, top: 4,
top: 4, },
}, scrollBeyondLastLine: false,
scrollBeyondLastLine: false, fixedOverflowWidgets: true,
fixedOverflowWidgets: true, fontFamily: "var(--font-geist-mono)",
fontFamily: "var(--font-geist-mono)", }}
}} theme={theme === "light" ? "vs" : "vs-dark"}
theme={theme === "light" ? "vs" : "vs-dark"} value={activeFileContent}
value={activeFileContent} />
/> </>
</> ) : (
) : ( <div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none"> <Loader2 className="animate-spin w-6 h-6 mr-3" />
<Loader2 className="animate-spin w-6 h-6 mr-3" /> Waiting for Clerk to load...
Waiting for Clerk to load... </div>
</div> )}
)}
</div> </div>
</ResizablePanel> </ResizablePanel>
<ResizableHandle /> <ResizableHandle />
@ -1293,18 +1234,13 @@ export default function CodeEditor({
lastCopiedRangeRef={lastCopiedRangeRef} lastCopiedRangeRef={lastCopiedRangeRef}
files={files} files={files}
templateType={sandboxData.type} templateType={sandboxData.type}
projectName={sandboxData.name}
handleApplyCode={handleApplyCode}
mergeDecorationsCollection={mergeDecorationsCollection}
setMergeDecorationsCollection={setMergeDecorationsCollection}
selectFile={selectFile}
/> />
</ResizablePanel> </ResizablePanel>
</> </>
)} )}
</ResizablePanelGroup> </ResizablePanelGroup>
</PreviewProvider> </PreviewProvider>
</div> </>
) )
} }

View File

@ -9,7 +9,7 @@ import {
import { useTerminal } from "@/context/TerminalContext" import { useTerminal } from "@/context/TerminalContext"
import { Sandbox, User } from "@/lib/types" import { Sandbox, User } from "@/lib/types"
import { Globe } from "lucide-react" import { Globe } from "lucide-react"
import { useEffect, useState } from "react" import { useState } from "react"
export default function DeployButtonModal({ export default function DeployButtonModal({
userData, userData,
@ -18,21 +18,8 @@ export default function DeployButtonModal({
userData: User userData: User
data: Sandbox data: Sandbox
}) { }) {
const { deploy, getAppExists } = useTerminal() const { deploy } = useTerminal()
const [isDeploying, setIsDeploying] = useState(false) const [isDeploying, setIsDeploying] = useState(false)
const [isDeployed, setIsDeployed] = useState(false)
const [deployButtonVisible, setDeployButtonEnabled] = useState(false)
useEffect(() => {
const checkDeployment = async () => {
if (getAppExists) {
const exists = await getAppExists(data.id)
setDeployButtonEnabled(exists.success)
setIsDeployed((exists.success && exists.exists) ?? false)
}
}
checkDeployment()
}, [data.id, getAppExists])
const handleDeploy = () => { const handleDeploy = () => {
if (isDeploying) { if (isDeploying) {
@ -43,7 +30,6 @@ export default function DeployButtonModal({
setIsDeploying(true) setIsDeploying(true)
deploy(() => { deploy(() => {
setIsDeploying(false) setIsDeploying(false)
setIsDeployed(true)
}) })
} }
} }
@ -52,12 +38,10 @@ export default function DeployButtonModal({
<> <>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
{deployButtonVisible && ( <Button variant="outline">
<Button variant="outline"> <Globe className="w-4 h-4 mr-2" />
<Globe className="w-4 h-4 mr-2" /> Deploy
Deploy </Button>
</Button>
)}
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <PopoverContent
className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg" className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg"
@ -68,9 +52,8 @@ export default function DeployButtonModal({
<DeploymentOption <DeploymentOption
icon={<Globe className="text-gray-500 w-5 h-5" />} icon={<Globe className="text-gray-500 w-5 h-5" />}
domain={`${data.id}.gitwit.app`} domain={`${data.id}.gitwit.app`}
timestamp="Deployed 1m ago" timestamp="Deployed 1h ago"
user={userData.name} user={userData.name}
isDeployed={isDeployed}
/> />
</div> </div>
<Button <Button
@ -78,7 +61,7 @@ export default function DeployButtonModal({
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]" className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
onClick={handleDeploy} onClick={handleDeploy}
> >
{isDeploying ? "Deploying..." : isDeployed ? "Update" : "Deploy"} {isDeploying ? "Deploying..." : "Update"}
</Button> </Button>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@ -91,33 +74,27 @@ function DeploymentOption({
domain, domain,
timestamp, timestamp,
user, user,
isDeployed,
}: { }: {
icon: React.ReactNode icon: React.ReactNode
domain: string domain: string
timestamp: string timestamp: string
user: string user: string
isDeployed: boolean
}) { }) {
return ( return (
<div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900"> <div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900">
<div className="flex items-start gap-2 relative"> <div className="flex items-start gap-2 relative">
<div className="flex-shrink-0">{icon}</div> <div className="flex-shrink-0">{icon}</div>
{isDeployed ? ( <a
<a href={`https://${domain}`}
href={`https://${domain}`} target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" className="font-semibold text-gray-300 hover:underline"
className="font-semibold text-gray-300 hover:underline" >
> {domain}
{domain} </a>
</a>
) : (
<span className="font-semibold text-gray-300">{domain}</span>
)}
</div> </div>
<p className="text-sm text-gray-400 mt-0 ml-7"> <p className="text-sm text-gray-400 mt-0 ml-7">
{isDeployed ? `${timestamp}${user}` : "Never deployed"} {timestamp} {user}
</p> </p>
</div> </div>
) )

View File

@ -35,7 +35,7 @@ export default function RunButtonModal({
} }
} }
}, [terminals, isRunning]) }, [terminals, isRunning])
// commands to run in the terminal
const handleRun = async () => { const handleRun = async () => {
if (isRunning && lastCreatedTerminalRef.current) { if (isRunning && lastCreatedTerminalRef.current) {
await closeTerminal(lastCreatedTerminalRef.current) await closeTerminal(lastCreatedTerminalRef.current)

View File

@ -9,7 +9,6 @@ import SidebarFolder from "./folder"
import New from "./new" import New from "./new"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { cn, sortFileExplorer } from "@/lib/utils" import { cn, sortFileExplorer } from "@/lib/utils"
import { import {
@ -105,8 +104,8 @@ export default function Sidebar({
return ( return (
<div className="h-full w-56 select-none flex flex-col text-sm"> <div className="h-full w-56 select-none flex flex-col text-sm">
<ScrollArea className="flex-grow overflow-auto px-2 pt-0 pb-4 relative"> <div className="flex-grow overflow-auto p-2 pb-[84px]">
<div className="flex w-full items-center justify-between h-8 pb-1 isolate z-10 sticky pt-2 top-0 bg-background"> <div className="flex w-full items-center justify-between h-8 mb-1">
<div className="text-muted-foreground">Explorer</div> <div className="text-muted-foreground">Explorer</div>
<div className="flex space-x-1"> <div className="flex space-x-1">
<button <button
@ -180,8 +179,8 @@ export default function Sidebar({
</> </>
)} )}
</div> </div>
</ScrollArea> </div>
<div className="flex flex-col p-2 bg-background"> <div className="fixed bottom-0 w-48 flex flex-col p-2 bg-background">
<Button <Button
variant="ghost" variant="ghost"
className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2" className="w-full justify-start text-sm text-muted-foreground font-normal h-8 px-2 mb-2"

View File

@ -7,7 +7,7 @@ import "./xterm.css"
import { debounce } from "@/lib/utils" import { debounce } from "@/lib/utils"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { ElementRef, useEffect, useRef, useCallback } from "react" import { ElementRef, useEffect, useRef } from "react"
import { Socket } from "socket.io-client" import { Socket } from "socket.io-client"
export default function EditorTerminal({ export default function EditorTerminal({
socket, socket,
@ -22,12 +22,13 @@ export default function EditorTerminal({
setTerm: (term: Terminal) => void setTerm: (term: Terminal) => void
visible: boolean visible: boolean
}) { }) {
const { resolvedTheme: theme } = useTheme() const { theme } = useTheme()
const terminalContainerRef = useRef<ElementRef<"div">>(null) const terminalContainerRef = useRef<ElementRef<"div">>(null)
const fitAddonRef = useRef<FitAddon | null>(null) const fitAddonRef = useRef<FitAddon | null>(null)
useEffect(() => { useEffect(() => {
if (!terminalContainerRef.current) return if (!terminalContainerRef.current) return
// console.log("new terminal", id, term ? "reusing" : "creating");
const terminal = new Terminal({ const terminal = new Terminal({
cursorBlink: true, cursorBlink: true,
@ -36,54 +37,13 @@ export default function EditorTerminal({
fontSize: 14, fontSize: 14,
lineHeight: 1.5, lineHeight: 1.5,
letterSpacing: 0, letterSpacing: 0,
allowTransparency: true,
rightClickSelectsWord: true,
allowProposedApi: true, // for custom key events
})
// right-click paste handler
const handleContextMenu = (e: MouseEvent) => {
e.preventDefault()
navigator.clipboard.readText().then((text) => {
if (text) {
socket.emit("terminalData", { id, data: text })
}
})
}
terminalContainerRef.current.addEventListener(
"contextmenu",
handleContextMenu
)
// keyboard paste handler
terminal.attachCustomKeyEventHandler((event: KeyboardEvent) => {
if (event.type === "keydown") {
if (
(event.ctrlKey || event.metaKey) &&
event.key.toLowerCase() === "v"
) {
event.preventDefault()
navigator.clipboard.readText().then((text) => {
if (text) {
socket.emit("terminalData", { id, data: text })
}
})
return false
}
}
return true
}) })
setTerm(terminal) setTerm(terminal)
const dispose = () => {
return () => {
terminal.dispose() terminal.dispose()
terminalContainerRef.current?.removeEventListener(
"contextmenu",
handleContextMenu
)
} }
return dispose
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -121,6 +81,7 @@ export default function EditorTerminal({
const { width, height } = entry.contentRect const { width, height } = entry.contentRect
// Only call fit if the size has actually changed
if ( if (
width !== terminalContainerRef.current.offsetWidth || width !== terminalContainerRef.current.offsetWidth ||
height !== terminalContainerRef.current.offsetHeight height !== terminalContainerRef.current.offsetHeight
@ -131,9 +92,10 @@ export default function EditorTerminal({
console.error("Error during fit:", err) console.error("Error during fit:", err)
} }
} }
}, 50) }, 50) // Debounce for 50ms
) )
// start observing for resize
resizeObserver.observe(terminalContainerRef.current) resizeObserver.observe(terminalContainerRef.current)
return () => { return () => {
disposableOnData.dispose() disposableOnData.dispose()
@ -162,7 +124,6 @@ export default function EditorTerminal({
ref={terminalContainerRef} ref={terminalContainerRef}
style={{ display: visible ? "block" : "none" }} style={{ display: visible ? "block" : "none" }}
className="w-full h-full text-left" className="w-full h-full text-left"
tabIndex={0}
> >
{term === null ? ( {term === null ? (
<div className="flex items-center text-muted-foreground p-2"> <div className="flex items-center text-muted-foreground p-2">

View File

@ -1,749 +0,0 @@
"use client"
import NewProjectModal from "@/components/dashboard/newProject"
import ProjectCard from "@/components/dashboard/projectCard/"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { deleteSandbox, updateSandbox, updateUser } from "@/lib/actions"
import { socialIcons } from "@/lib/data"
import { editUserSchema, EditUserSchema } from "@/lib/schema"
import { TIERS } from "@/lib/tiers"
import { SandboxWithLiked, User, UserLink } from "@/lib/types"
import { cn, parseSocialLink } from "@/lib/utils"
import { useUser } from "@clerk/nextjs"
import { zodResolver } from "@hookform/resolvers/zod"
import {
Edit,
Globe,
Heart,
Info,
Loader2,
LucideIcon,
Package2,
PlusCircle,
Sparkles,
Trash2,
X,
} from "lucide-react"
import { useRouter } from "next/navigation"
import {
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState,
useTransition,
} from "react"
import { useFormState, useFormStatus } from "react-dom"
import { useFieldArray, useForm } from "react-hook-form"
import { toast } from "sonner"
import Avatar from "../ui/avatar"
import { Badge } from "../ui/badge"
import { Input } from "../ui/input"
import { Progress } from "../ui/progress"
import { Textarea } from "../ui/textarea"
// #region Profile Page
export default function ProfilePage({
publicSandboxes,
privateSandboxes,
profileOwner,
loggedInUser,
}: {
publicSandboxes: SandboxWithLiked[]
privateSandboxes: SandboxWithLiked[]
profileOwner: User
loggedInUser: User | null
}) {
const isOwnProfile = profileOwner.id === loggedInUser?.id
const sandboxes = useMemo(() => {
const allSandboxes = isOwnProfile
? [...publicSandboxes, ...privateSandboxes]
: publicSandboxes
return allSandboxes
}, [isOwnProfile, publicSandboxes, privateSandboxes])
return (
<>
<div className="container mx-auto p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-1">
<ProfileCard
name={profileOwner.name}
username={profileOwner.username}
avatarUrl={profileOwner.avatarUrl}
sandboxes={sandboxes}
joinedDate={profileOwner.createdAt}
generations={isOwnProfile ? loggedInUser.generations : undefined}
isOwnProfile={isOwnProfile}
tier={profileOwner.tier}
bio={profileOwner.bio}
personalWebsite={profileOwner.personalWebsite}
socialLinks={profileOwner.links}
/>
</div>
<div className="md:col-span-2">
<SandboxesPanel
{...{
publicSandboxes,
privateSandboxes,
isOwnProfile,
}}
/>
</div>
</div>
</>
)
}
// #endregion
// #region Profile Card
function ProfileCard({
name,
username,
avatarUrl,
sandboxes,
joinedDate,
generations,
isOwnProfile,
bio,
personalWebsite,
socialLinks = [],
tier,
}: {
name: string
username: string
avatarUrl: string | null
bio: string | null
personalWebsite: string | null
socialLinks: UserLink[]
sandboxes: SandboxWithLiked[]
joinedDate: Date
generations?: number
isOwnProfile: boolean
tier: string
}) {
const { user } = useUser()
const [isEditing, setIsEditing] = useState(false)
const joinedAt = useMemo(() => {
const date = new Date(joinedDate).toLocaleDateString("en-US", {
month: "long",
year: "numeric",
})
return `Joined ${date}`
}, [joinedDate])
const toggleEdit = useCallback(() => {
setIsEditing((s) => !s)
}, [])
const stats = useMemo(() => {
const totalSandboxes = sandboxes.length
const totalLikes = sandboxes.reduce(
(sum, sandbox) => sum + sandbox.likeCount,
0
)
return {
sandboxes:
totalSandboxes === 1 ? "1 sandbox" : `${totalSandboxes} sandboxes`,
likes: totalLikes === 1 ? "1 like" : `${totalLikes} likes`,
}
}, [sandboxes])
const showAddMoreInfoBanner = useMemo(() => {
return !bio && !personalWebsite && (socialLinks?.length ?? 0) === 0
}, [personalWebsite, bio, socialLinks])
return (
<Card className="mb-6 md:mb-0 sticky top-6">
{isOwnProfile && (
<div className="absolute top-2 right-2 flex flex-col gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={toggleEdit}
aria-label={isEditing ? "close edit form" : "open edit form"}
size="smIcon"
variant="secondary"
className="rounded-full relative"
>
{isEditing ? (
<X className="size-4" />
) : showAddMoreInfoBanner ? (
<>
<Sparkles className="size-4 text-yellow-400 z-[2]" />
<div className="z-[1] absolute inset-0 rounded-full bg-secondary animate-ping" />
</>
) : (
<Edit className="size-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{showAddMoreInfoBanner
? "Add more information to your profile"
: "Edit your profile"}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
<CardContent className="flex flex-col gap-4 pt-6">
{isEditing ? (
<div className="flex flex-col gap-2 items-center ">
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
<EditProfileForm
{...{
name,
username,
avatarUrl,
bio,
personalWebsite,
socialLinks,
toggleEdit,
}}
/>
</div>
) : (
<>
<div className="flex flex-col gap-2 items-center">
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
<div className="space-y-1">
<CardTitle className="text-2xl text-center">{name}</CardTitle>
<CardDescription className="text-center">{`@${username}`}</CardDescription>
</div>
{bio && <p className="text-sm text-center">{bio}</p>}
{(socialLinks.length > 0 || personalWebsite) && (
<div className="flex gap-2 justify-center">
{personalWebsite && (
<Button variant="secondary" size="smIcon" asChild>
<a
href={personalWebsite}
target="_blank"
rel="noopener noreferrer"
>
<Globe className="size-4" />
<span className="sr-only">Personal Website</span>
</a>
</Button>
)}
{socialLinks.map((link, index) => {
const Icon = socialIcons[link.platform]
return (
<Button
key={index}
variant="secondary"
size="smIcon"
asChild
>
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
>
<Icon className="size-4" />
<span className="sr-only">{link.platform}</span>
</a>
</Button>
)
})}
</div>
)}
</div>
<div className="flex flex-col gap-1 items-center">
{typeof generations === "number" && (
<div className="flex justify-center">
<SubscriptionBadge
generations={generations}
tier={tier as keyof typeof TIERS}
/>
</div>
)}
<div className="flex gap-4">
<StatsItem icon={Package2} label={stats.sandboxes} />
<StatsItem icon={Heart} label={stats.likes} />
</div>
</div>
<p className="text-xs mt-2 text-muted-foreground text-center">
{joinedAt}
</p>
</>
)}
</CardContent>
</Card>
)
}
function EditProfileForm(props: {
name: string
username: string
avatarUrl: string | null
bio: string | null
personalWebsite: string | null
socialLinks: UserLink[]
toggleEdit: () => void
}) {
const router = useRouter()
const { user } = useUser()
const formRef = useRef<HTMLFormElement>(null)
const [formState, formAction] = useFormState(updateUser, {
message: "",
})
const [isPending, startTransition] = useTransition()
const { name, username, bio, personalWebsite, socialLinks, toggleEdit } =
props
const form = useForm<EditUserSchema>({
resolver: zodResolver(editUserSchema),
defaultValues: {
oldUsername: username,
id: user?.id,
name,
username,
bio: bio ?? "",
personalWebsite: personalWebsite ?? "",
links:
socialLinks.length > 0
? socialLinks
: [{ url: "", platform: "generic" }],
...(formState.fields ?? {}),
},
})
const { fields, append, remove } = useFieldArray({
name: "links",
control: form.control,
})
useEffect(() => {
const message = formState.message
if (!Boolean(message)) return
if ("error" in formState) {
toast.error(formState.message)
return
}
toast.success(formState.message as String)
toggleEdit()
if (formState?.newRoute) {
router.replace(formState.newRoute)
}
}, [formState])
return (
<Form {...form}>
<form
ref={formRef}
action={formAction}
onSubmit={(evt) => {
evt.preventDefault()
form.handleSubmit(() => {
startTransition(() => {
formAction(new FormData(formRef.current!))
})
})(evt)
}}
className="space-y-3 w-full"
>
<input type="hidden" name="id" value={user?.id} />
<input type="hidden" name="oldUsername" value={username} />
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="marie doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>User name</FormLabel>
<FormControl>
<div className="relative">
<Input
className="peer ps-6"
type="text"
placeholder="Username"
{...field}
/>
<span className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-2 text-sm text-muted-foreground peer-disabled:opacity-50">
@
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="hi, I love building things!"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="personalWebsite"
render={({ field }) => (
<FormItem>
<FormLabel>Personal Website</FormLabel>
<FormControl>
<Input placeholder="https://chillguy.dev" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div>
{fields.map((field, index) => (
<FormField
control={form.control}
key={field.id}
name={`links.${index}`}
render={({ field: { onChange, value, ...field } }) => {
const Icon = socialIcons[value.platform] ?? socialIcons.generic
return (
<FormItem>
<FormLabel className={cn(index !== 0 && "sr-only")}>
Social Links
</FormLabel>
<FormDescription className={cn(index !== 0 && "sr-only")}>
Add links to your blogs or social media profiles.
</FormDescription>
<FormControl>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
{...field}
className="peer ps-9"
value={value.url}
onChange={(e) =>
onChange(parseSocialLink(e.currentTarget.value))
}
/>
<div className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 peer-disabled:opacity-50">
<Icon
size={16}
strokeWidth={2}
aria-hidden="true"
/>
</div>
</div>
<Button
size="smIcon"
type="button"
variant="secondary"
onClick={() => remove(index)}
>
<Trash2 className="size-4" />
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)
}}
/>
))}
<Button
type="button"
variant="outline"
size="sm"
className="mt-2"
onClick={() => append({ url: "", platform: "generic" })}
>
Add URL
</Button>
</div>
<SubmitButton {...{ isPending }} />
</form>
</Form>
)
}
function SubmitButton({ isPending }: { isPending: boolean }) {
const formStatus = useFormStatus()
const { pending } = formStatus
const pend = pending || isPending
return (
<Button size="sm" type="submit" className="w-full mt-2" disabled={pend}>
{pend && <Loader2 className="animate-spin mr-2 h-4 w-4" />}
Save Changes
</Button>
)
}
// #endregion
// #region Sandboxes Panel
function SandboxesPanel({
publicSandboxes,
privateSandboxes,
isOwnProfile,
}: {
publicSandboxes: SandboxWithLiked[]
privateSandboxes: SandboxWithLiked[]
isOwnProfile: boolean
}) {
const [deletingId, setDeletingId] = useState<string>("")
const hasPublicSandboxes = publicSandboxes.length > 0
const hasPrivateSandboxes = privateSandboxes.length > 0
const onVisibilityChange = useMemo(
() =>
async (sandbox: Pick<SandboxWithLiked, "id" | "name" | "visibility">) => {
const newVisibility =
sandbox.visibility === "public" ? "private" : "public"
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
})
},
[]
)
const onDelete = useMemo(
() => async (sandbox: Pick<SandboxWithLiked, "id" | "name">) => {
setDeletingId(sandbox.id)
toast(`Project ${sandbox.name} deleted.`)
await deleteSandbox(sandbox.id)
setDeletingId("")
},
[]
)
if (!isOwnProfile) {
return (
<div className="">
{hasPublicSandboxes ? (
<>
<h2 className="font-semibold text-xl mb-4">Sandboxes</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{publicSandboxes.map((sandbox) => {
return (
<Fragment key={sandbox.id}>
{isOwnProfile ? (
<ProjectCard
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
deletingId={deletingId}
isAuthenticated
{...sandbox}
/>
) : (
<ProjectCard isAuthenticated={false} {...sandbox} />
)}
</Fragment>
)
})}
</div>
</>
) : (
<EmptyState type="private" isOwnProfile={isOwnProfile} />
)}
</div>
)
}
return (
<Tabs defaultValue="public">
<TabsList className="mb-4">
<TabsTrigger value="public">Public</TabsTrigger>
<TabsTrigger value="private">Private</TabsTrigger>
</TabsList>
<TabsContent value="public">
{hasPublicSandboxes ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{publicSandboxes.map((sandbox) => {
return (
<Fragment key={sandbox.id}>
{isOwnProfile ? (
<ProjectCard
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
deletingId={deletingId}
isAuthenticated
{...sandbox}
/>
) : (
<ProjectCard isAuthenticated={false} {...sandbox} />
)}
</Fragment>
)
})}
</div>
) : (
<EmptyState type="public" isOwnProfile={isOwnProfile} />
)}
</TabsContent>
<TabsContent value="private">
{hasPrivateSandboxes ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{privateSandboxes.map((sandbox) => (
<ProjectCard
key={sandbox.id}
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
deletingId={deletingId}
isAuthenticated
{...sandbox}
/>
))}
</div>
) : (
<EmptyState type="private" isOwnProfile={isOwnProfile} />
)}
</TabsContent>
</Tabs>
)
}
// #endregion
// #region Empty State
function EmptyState({
type,
isOwnProfile,
}: {
type: "public" | "private"
isOwnProfile: boolean
}) {
const [newProjectModalOpen, setNewProjectModalOpen] = useState(false)
const text = useMemo(() => {
let title: string
let description: string
switch (type) {
case "public":
title = "No public sandboxes yet"
description = isOwnProfile
? "Create your first public sandbox to share your work with the world!"
: "user has no public sandboxes"
case "private":
title = "No private sandboxes yet"
description = isOwnProfile
? "Create your first private sandbox to start working on your personal projects!"
: "user has no private sandboxes"
}
return {
title,
description,
}
}, [type, isOwnProfile])
const openModal = useCallback(() => setNewProjectModalOpen(true), [])
return (
<>
<Card className="flex flex-col items-center justify-center p-6 text-center h-[300px]">
<PlusCircle className="h-12 w-12 text-muted-foreground mb-4" />
<CardTitle className="text-xl mb-2">{text.title}</CardTitle>
<CardDescription className="mb-4">{text.description}</CardDescription>
{isOwnProfile && (
<Button onClick={openModal}>
<PlusCircle className="h-4 w-4 mr-2" />
Create Sandbox
</Button>
)}
</Card>
<NewProjectModal
open={newProjectModalOpen}
setOpen={setNewProjectModalOpen}
/>
</>
)
}
// #endregion
// #region StatsItem
interface StatsItemProps {
icon: LucideIcon
label: string
}
const StatsItem = ({ icon: Icon, label }: StatsItemProps) => (
<div className="flex items-center gap-2">
<Icon size={16} />
<span className="text-sm text-muted-foreground">{label}</span>
</div>
)
// #endregion
// #region Sub Badge
const SubscriptionBadge = ({
generations,
tier = "FREE",
}: {
generations: number
tier?: keyof typeof TIERS
}) => {
return (
<div className="flex gap-2 items-center">
<Badge variant="secondary" className="text-sm cursor-pointer">
{tier}
</Badge>
<HoverCard>
<HoverCardTrigger>
<Button variant="ghost" size="smIcon" className="size-[26px]">
<Info size={16} />
</Button>
</HoverCardTrigger>
<HoverCardContent>
<div className="w-full space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">AI Generations</span>
<span>{`${generations} / ${TIERS[tier].generations}`}</span>
</div>
<Progress
value={generations}
max={TIERS[tier].generations}
className="w-full"
/>
</div>
<Button size="sm" className="w-full mt-4">
<Sparkles className="mr-2 h-4 w-4" /> Upgrade to Pro
</Button>
</HoverCardContent>
</HoverCard>
</div>
)
}
// #endregion

View File

@ -1,38 +0,0 @@
import Logo from "@/assets/logo.svg"
import { ThemeSwitcher } from "@/components/ui/theme-switcher"
import UserButton from "@/components/ui/userButton"
import { User } from "@/lib/types"
import Image from "next/image"
import Link from "next/link"
import { Button } from "../ui/button"
export default function ProfileNavbar({ userData }: { userData: User }) {
return (
<nav className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
<div className="flex items-center space-x-2">
<Link
href="/"
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
>
<Image src={Logo} alt="Logo" width={36} height={36} />
</Link>
<h1 className="text-xl">
<span className="font-semibold">Sandbox</span>{" "}
<span className="text-xs font-medium text-muted-foreground">
by gitwit
</span>
</h1>
</div>
<div className="flex items-center space-x-4">
<ThemeSwitcher />
{Boolean(userData?.id) ? (
<UserButton userData={userData} />
) : (
<Link href="/sign-in">
<Button>Login</Button>
</Link>
)}
</div>
</nav>
)
}

View File

@ -1,4 +1,5 @@
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,
@ -21,12 +22,12 @@ export default function Avatar({
return ( return (
<div <div
className={cn( className={cn(
"size-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", className,
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"
)} )}
> >
{avatarUrl ? ( {avatarUrl ? (
<img <Image
src={avatarUrl} src={avatarUrl}
alt={name || "User"} alt={name || "User"}
width={20} width={20}

View File

@ -1,36 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -5,7 +5,7 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition active:scale-[0.99] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{ {
variants: { variants: {
variant: { variant: {
@ -26,7 +26,7 @@ const buttonVariants = cva(
sm: "h-8 rounded-md px-3 text-xs", sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8", lg: "h-10 rounded-md px-8",
icon: "h-9 w-9", icon: "h-9 w-9",
smIcon: "size-8", smIcon: "h-8 w-8",
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@ -1,8 +1,6 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import * as React from "react"
import { import {
Controller, Controller,
ControllerProps, ControllerProps,
@ -12,8 +10,8 @@ import {
useFormContext, useFormContext,
} from "react-hook-form" } from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { cn } from "@/lib/utils"
const Form = FormProvider const Form = FormProvider
@ -95,7 +93,7 @@ const FormLabel = React.forwardRef<
return ( return (
<Label <Label
ref={ref} ref={ref}
className={cn(error && "text-destructive", className)} className={cn(className)}
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
@ -167,12 +165,12 @@ const FormMessage = React.forwardRef<
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage"
export { export {
useFormField,
Form, Form,
FormItem,
FormLabel,
FormControl, FormControl,
FormDescription, FormDescription,
FormMessage,
FormField, FormField,
FormItem,
FormLabel,
FormMessage,
useFormField,
} }

View File

@ -1,29 +0,0 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@ -18,7 +18,7 @@ const Progress = React.forwardRef<
{...props} {...props}
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all rounded-full" className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>

View File

@ -1,48 +0,0 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -1,55 +0,0 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -1,22 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -1,11 +1,8 @@
"use client" "use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes" import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider> return <NextThemesProvider {...props}>{children}</NextThemesProvider>
} }

View File

@ -17,7 +17,7 @@ export function ThemeSwitcher() {
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="text-foreground"> <Button variant="outline" size="icon" className="text-muted-foreground">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span> <span className="sr-only">Toggle theme</span>

View File

@ -1,7 +1,7 @@
"use client" "use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip" import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -29,4 +29,4 @@ const TooltipContent = React.forwardRef<
)) ))
TooltipContent.displayName = TooltipPrimitive.Content.displayName TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }

View File

@ -9,15 +9,7 @@ import {
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { User } from "@/lib/types" import { User } from "@/lib/types"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
import { import { Crown, LogOut, Sparkles } from "lucide-react"
Crown,
LayoutDashboard,
LogOut,
Sparkles,
User as UserIcon,
} from "lucide-react"
import Link from "next/link"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import Avatar from "./avatar" import Avatar from "./avatar"
@ -43,11 +35,7 @@ const TIER_INFO = {
}, },
} as const } as const
export default function UserButton({ export default function UserButton({ userData: initialUserData }: { userData: User }) {
userData: initialUserData,
}: {
userData: User
}) {
const [userData, setUserData] = useState<User>(initialUserData) const [userData, setUserData] = useState<User>(initialUserData)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const { signOut } = useClerk() const { signOut } = useClerk()
@ -61,7 +49,7 @@ export default function UserButton({
headers: { headers: {
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`, Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
}, },
cache: "no-store", cache: 'no-store'
} }
) )
if (res.ok) { if (res.ok) {
@ -79,15 +67,12 @@ export default function UserButton({
} }
}, [isOpen]) }, [isOpen])
const tierInfo = const tierInfo = TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
const TierIcon = tierInfo.icon const TierIcon = tierInfo.icon
const usagePercentage = Math.floor( const usagePercentage = Math.floor((userData.generations || 0) * 100 / tierInfo.limit)
((userData.generations || 0) * 100) / tierInfo.limit
)
const handleUpgrade = async () => { const handleUpgrade = async () => {
router.push(`/@${userData.username}`) router.push('/upgrade')
} }
return ( return (
@ -105,30 +90,13 @@ export default function UserButton({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer" asChild>
<Link href={"/dashboard"}>
<LayoutDashboard className="mr-2 size-4" />
<span>Dashboard</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="cursor-pointer" asChild>
<Link href={`/@${userData.username}`}>
<UserIcon className="mr-2 size-4" />
<span>Profile</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<div className="py-1.5 px-2 w-full"> <div className="py-1.5 px-2 w-full">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<TierIcon className={`h-4 w-4 ${tierInfo.color}`} /> <TierIcon className={`h-4 w-4 ${tierInfo.color}`} />
<span className="text-sm font-medium"> <span className="text-sm font-medium">{userData.tier || "FREE"} Plan</span>
{userData.tier || "FREE"} Plan
</span>
</div> </div>
{(userData.tier === "FREE" || userData.tier === "PRO") && ( {/* {(userData.tier === "FREE" || userData.tier === "PRO") && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -137,50 +105,47 @@ export default function UserButton({
> >
Upgrade Upgrade
</Button> </Button>
)} )} */}
</div> </div>
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="px-2 py-1.5">
<div className="w-full">
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
<span>AI Usage</span>
<span>
{userData.generations}/{tierInfo.limit}
</span>
</div>
<div className="rounded-full w-full h-2 overflow-hidden bg-secondary mb-1"> <div className="py-1.5 px-2 w-full">
<div <div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
className={`h-full rounded-full transition-all duration-300 ${ <span>AI Usage</span>
usagePercentage > 90 <span>{userData.generations}/{tierInfo.limit}</span>
? "bg-red-500" </div>
: usagePercentage > 75
? "bg-yellow-500" <div className="rounded-full w-full h-2 overflow-hidden bg-secondary">
: tierInfo.color.replace("text-", "bg-") <div
}`} className={`h-full rounded-full transition-all duration-300 ${
style={{ usagePercentage > 90 ? 'bg-red-500' :
width: `${Math.min(usagePercentage, 100)}%`, usagePercentage > 75 ? 'bg-yellow-500' :
}} tierInfo.color.replace('text-', 'bg-')
/> }`}
</div> style={{
width: `${Math.min(usagePercentage, 100)}%`,
}}
/>
</div> </div>
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
{/* <DropdownMenuItem className="cursor-pointer"> {/* <DropdownMenuItem className="cursor-pointer">
<Pencil className="mr-2 size-4" /> <Pencil className="mr-2 h-4 w-4" />
<span>Edit Profile</span> <span>Edit Profile</span>
</DropdownMenuItem> */} </DropdownMenuItem> */}
<DropdownMenuItem <DropdownMenuItem
onClick={() => signOut(() => router.push("/"))} onClick={() => signOut(() => router.push("/"))}
className="!text-destructive cursor-pointer" className="!text-destructive cursor-pointer"
> >
<LogOut className="mr-2 size-4" /> <LogOut className="mr-2 h-4 w-4" />
<span>Log Out</span> <span>Log Out</span>
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) )
} }

View File

@ -20,9 +20,6 @@ interface TerminalContextType {
createNewTerminal: (command?: string) => Promise<void> createNewTerminal: (command?: string) => Promise<void>
closeTerminal: (id: string) => void closeTerminal: (id: string) => void
deploy: (callback: () => void) => void deploy: (callback: () => void) => void
getAppExists:
| ((appName: string) => Promise<{ success: boolean; exists?: boolean }>)
| null
} }
const TerminalContext = createContext<TerminalContextType | undefined>( const TerminalContext = createContext<TerminalContextType | undefined>(
@ -38,19 +35,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
>([]) >([])
const [activeTerminalId, setActiveTerminalId] = useState<string>("") const [activeTerminalId, setActiveTerminalId] = useState<string>("")
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false) const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false)
const [isSocketReady, setIsSocketReady] = useState<boolean>(false)
// Listen for the "ready" signal from the socket
React.useEffect(() => {
if (socket) {
socket.on("ready", () => {
setIsSocketReady(true)
})
}
return () => {
if (socket) socket.off("ready")
}
}, [socket])
const createNewTerminal = async (command?: string): Promise<void> => { const createNewTerminal = async (command?: string): Promise<void> => {
if (!socket) return if (!socket) return
@ -94,20 +78,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
}) })
} }
const getAppExists = async (
appName: string
): Promise<{ success: boolean; exists?: boolean }> => {
console.log("Is there a socket: " + !!socket)
if (!socket) {
console.error("Couldn't check if app exists: No socket")
return { success: false }
}
const response: { success: boolean; exists?: boolean } = await new Promise(
(resolve) => socket.emit("getAppExists", { appName }, resolve)
)
return response
}
const value = { const value = {
terminals, terminals,
setTerminals, setTerminals,
@ -118,7 +88,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
createNewTerminal, createNewTerminal,
closeTerminal, closeTerminal,
deploy, deploy,
getAppExists: isSocketReady ? getAppExists : null,
} }
return ( return (

View File

@ -1,10 +1,6 @@
"use server" "use server"
import { revalidatePath } from "next/cache" import { revalidatePath } from "next/cache"
import { z } from "zod"
import { editUserSchema } from "./schema"
import { UserLink } from "./types"
import { parseSocialLink } from "./utils"
export async function createSandbox(body: { export async function createSandbox(body: {
type: string type: string
@ -95,110 +91,3 @@ export async function unshareSandbox(sandboxId: string, userId: string) {
revalidatePath(`/code/${sandboxId}`) revalidatePath(`/code/${sandboxId}`)
} }
export async function toggleLike(sandboxId: string, userId: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/sandbox/like`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
},
body: JSON.stringify({ sandboxId, userId }),
}
)
revalidatePath(`/[username]`, "page")
revalidatePath(`/dashboard`, "page")
}
const UpdateErrorSchema = z.object({
error: z
.union([
z.string(),
z.array(
z.object({
path: z.array(z.string()),
message: z.string(),
})
),
])
.optional(),
})
interface FormState {
message: string
error?: any
newRoute?: string
fields?: Record<string, unknown>
}
export async function updateUser(
prevState: any,
formData: FormData
): Promise<FormState> {
let data = Object.fromEntries(formData)
let links: UserLink[] = []
Object.entries(data).forEach(([key, value]) => {
if (key.startsWith("link")) {
const [_, index] = key.split(".")
if (value) {
links.splice(parseInt(index), 0, parseSocialLink(value as string))
delete data[key]
}
}
})
// @ts-ignore
data.links = links
try {
const validatedData = editUserSchema.parse(data)
const changedUsername = validatedData.username !== validatedData.oldUsername
const res = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
},
body: JSON.stringify({
id: validatedData.id,
username: data.username ?? undefined,
name: data.name ?? undefined,
bio: data.bio ?? undefined,
personalWebsite: data.personalWebsite ?? undefined,
links: data.links ?? undefined,
}),
}
)
const responseData = await res.json()
// Validate the response using our error schema
const parseResult = UpdateErrorSchema.safeParse(responseData)
if (!parseResult.success) {
return {
message: "Unexpected error occurred",
error: parseResult.error,
fields: validatedData,
}
}
if (changedUsername) {
const newRoute = `/@${validatedData.username}`
return { message: "Successfully updated", newRoute }
}
revalidatePath(`/[username]`, "page")
return { message: "Successfully updated" }
} catch (error) {
if (error instanceof z.ZodError) {
return {
message: "Invalid data",
error: error.errors,
fields: data,
}
}
return { message: "An unexpected error occurred", fields: data }
}
}

View File

@ -1,14 +0,0 @@
export const KNOWN_PLATFORMS = [
"github",
"twitter",
"instagram",
"bluesky",
"linkedin",
"youtube",
"twitch",
"discord",
"mastodon",
"threads",
"gitlab",
"generic",
] as const

View File

@ -1,37 +1,3 @@
import {
AtSign,
Github,
GitlabIcon as GitlabLogo,
Globe,
Instagram,
Link,
Linkedin,
MessageCircle,
Twitch,
Twitter,
Youtube,
} from "lucide-react"
import { KnownPlatform } from "../types"
export const socialIcons: Record<
KnownPlatform | "website",
React.ComponentType<any>
> = {
github: Github,
twitter: Twitter,
instagram: Instagram,
bluesky: AtSign,
linkedin: Linkedin,
youtube: Youtube,
twitch: Twitch,
discord: MessageCircle,
mastodon: AtSign,
threads: AtSign,
gitlab: GitlabLogo,
generic: Link,
website: Globe,
}
export const projectTemplates: { export const projectTemplates: {
id: string id: string
name: string name: string
@ -67,11 +33,4 @@ export const projectTemplates: {
description: "A faster way to build and share data apps", description: "A faster way to build and share data apps",
disabled: false, disabled: false,
}, },
{
id: "php",
name: "PHP",
description: "PHP development environment",
icon: "/project-icons/php.svg",
disabled: false,
},
] ]

View File

@ -1,20 +0,0 @@
import { z } from "zod"
import { KNOWN_PLATFORMS } from "../constants"
export const editUserSchema = z.object({
id: z.string().trim(),
username: z.string().trim().min(1, "Username must be at least 1 character"),
oldUsername: z.string().trim(),
name: z.string().trim().min(1, "Name must be at least 1 character"),
bio: z.string().trim().optional(),
personalWebsite: z.string().trim().optional(),
links: z
.array(
z.object({
url: z.string().trim(),
platform: z.enum(KNOWN_PLATFORMS),
})
)
.catch([]),
})
export type EditUserSchema = z.infer<typeof editUserSchema>

View File

@ -1,290 +1,248 @@
export interface TemplateConfig { export interface TemplateConfig {
id: string id: string
name: string name: string,
runCommand: string runCommand: string,
fileStructure: { fileStructure: {
[key: string]: { [key: string]: {
purpose: string purpose: string
description: string description: string
}
}
conventions: string[]
dependencies?: {
[key: string]: string
}
scripts?: {
[key: string]: string
} }
} }
conventions: string[]
dependencies?: {
[key: string]: string
}
scripts?: {
[key: string]: string
}
}
export const templateConfigs: { [key: string]: TemplateConfig } = { export const templateConfigs: { [key: string]: TemplateConfig } = {
reactjs: { reactjs: {
id: "reactjs", id: "reactjs",
name: "React", name: "React",
runCommand: "npm run dev", runCommand: "npm run dev",
fileStructure: { fileStructure: {
"src/": { "src/": {
purpose: "source", purpose: "source",
description: "Contains all React components and application logic", description: "Contains all React components and application logic"
},
"src/components/": {
purpose: "components",
description: "Reusable React components"
},
"src/lib/": {
purpose: "utilities",
description: "Utility functions and shared code"
},
"src/App.tsx": {
purpose: "entry",
description: "Main application component"
},
"src/index.tsx": {
purpose: "entry",
description: "Application entry point"
},
"src/index.css": {
purpose: "styles",
description: "Global CSS styles"
},
"public/": {
purpose: "static",
description: "Static assets and index.html"
},
"tsconfig.json": {
purpose: "config",
description: "TypeScript configuration"
},
"vite.config.ts": {
purpose: "config",
description: "Vite bundler configuration"
},
"package.json": {
purpose: "config",
description: "Project dependencies and scripts"
}
}, },
"src/components/": { conventions: [
purpose: "components", "Use functional components with hooks",
description: "Reusable React components", "Follow React naming conventions (PascalCase for components)",
}, "Keep components small and focused",
"src/lib/": { "Use TypeScript for type safety"
purpose: "utilities", ],
description: "Utility functions and shared code", dependencies: {
}, "@radix-ui/react-icons": "^1.3.0",
"src/App.tsx": { "@radix-ui/react-slot": "^1.1.0",
purpose: "entry", "class-variance-authority": "^0.7.0",
description: "Main application component", "clsx": "^2.1.1",
}, "lucide-react": "^0.441.0",
"src/index.tsx": { "react": "^18.3.1",
purpose: "entry", "react-dom": "^18.3.1",
description: "Application entry point", "tailwind-merge": "^2.5.2",
}, "tailwindcss-animate": "^1.0.7"
"src/index.css": {
purpose: "styles",
description: "Global CSS styles",
},
"public/": {
purpose: "static",
description: "Static assets and index.html",
},
"tsconfig.json": {
purpose: "config",
description: "TypeScript configuration",
},
"vite.config.ts": {
purpose: "config",
description: "Vite bundler configuration",
},
"package.json": {
purpose: "config",
description: "Project dependencies and scripts",
}, },
scripts: {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
}
}, },
conventions: [ // Next.js template config
"Use functional components with hooks", nextjs: {
"Follow React naming conventions (PascalCase for components)", id: "nextjs",
"Keep components small and focused", name: "NextJS",
"Use TypeScript for type safety", runCommand: "npm run dev",
], fileStructure: {
dependencies: { "pages/": {
"@radix-ui/react-icons": "^1.3.0", purpose: "routing",
"@radix-ui/react-slot": "^1.1.0", description: "Page components and API routes"
"class-variance-authority": "^0.7.0", },
clsx: "^2.1.1", "pages/api/": {
"lucide-react": "^0.441.0", purpose: "api",
react: "^18.3.1", description: "API route handlers"
"react-dom": "^18.3.1", },
"tailwind-merge": "^2.5.2", "pages/_app.tsx": {
"tailwindcss-animate": "^1.0.7", purpose: "entry",
description: "Application wrapper component"
},
"pages/index.tsx": {
purpose: "page",
description: "Homepage component"
},
"public/": {
purpose: "static",
description: "Static assets and files"
},
"styles/": {
purpose: "styles",
description: "CSS modules and global styles"
},
"styles/globals.css": {
purpose: "styles",
description: "Global CSS styles"
},
"styles/Home.module.css": {
purpose: "styles",
description: "Homepage-specific styles"
},
"next.config.js": {
purpose: "config",
description: "Next.js configuration"
},
"next-env.d.ts": {
purpose: "types",
description: "Next.js TypeScript declarations"
},
"tsconfig.json": {
purpose: "config",
description: "TypeScript configuration"
},
"package.json": {
purpose: "config",
description: "Project dependencies and scripts"
}
},
conventions: [
"Use file-system based routing",
"Keep API routes in pages/api",
"Use CSS Modules for component styles",
"Follow Next.js data fetching patterns"
],
dependencies: {
"next": "^14.1.0",
"react": "^18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "^3.4.1"
},
scripts: {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
}
}, },
scripts: { // Streamlit template config
dev: "vite", streamlit: {
build: "tsc && vite build", id: "streamlit",
preview: "vite preview", name: "Streamlit",
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
fileStructure: {
"main.py": {
purpose: "entry",
description: "Main Streamlit application file"
},
"requirements.txt": {
purpose: "dependencies",
description: "Python package dependencies"
},
"Procfile": {
purpose: "deployment",
description: "Deployment configuration for hosting platforms"
},
"venv/": {
purpose: "environment",
description: "Python virtual environment directory"
}
},
conventions: [
"Use Streamlit components for UI",
"Follow PEP 8 style guide",
"Keep dependencies in requirements.txt",
"Use virtual environment for isolation"
],
dependencies: {
"streamlit": "^1.40.0",
"altair": "^5.5.0"
},
scripts: {
"start": "streamlit run main.py",
"dev": "./venv/bin/streamlit run main.py --server.runOnSave true"
}
}, },
}, // HTML template config
// Next.js template config vanillajs: {
nextjs: { id: "vanillajs",
id: "nextjs", name: "HTML/JS",
name: "NextJS", runCommand: "npm run dev",
runCommand: "npm run dev", fileStructure: {
fileStructure: { "index.html": {
"pages/": { purpose: "entry",
purpose: "routing", description: "Main HTML entry point"
description: "Page components and API routes", },
"style.css": {
purpose: "styles",
description: "Global CSS styles"
},
"script.js": {
purpose: "scripts",
description: "JavaScript application logic"
},
"package.json": {
purpose: "config",
description: "Project dependencies and scripts"
},
"package-lock.json": {
purpose: "config",
description: "Locked dependency versions"
},
"vite.config.js": {
purpose: "config",
description: "Vite bundler configuration"
}
}, },
"pages/api/": { conventions: [
purpose: "api", "Use semantic HTML elements",
description: "API route handlers", "Keep CSS modular and organized",
"Write clean, modular JavaScript",
"Follow modern ES6+ practices"
],
dependencies: {
"vite": "^5.0.12"
}, },
"pages/_app.tsx": { scripts: {
purpose: "entry", "dev": "vite",
description: "Application wrapper component", "build": "vite build",
}, "preview": "vite preview"
"pages/index.tsx": { }
purpose: "page", }
description: "Homepage component",
},
"public/": {
purpose: "static",
description: "Static assets and files",
},
"styles/": {
purpose: "styles",
description: "CSS modules and global styles",
},
"styles/globals.css": {
purpose: "styles",
description: "Global CSS styles",
},
"styles/Home.module.css": {
purpose: "styles",
description: "Homepage-specific styles",
},
"next.config.js": {
purpose: "config",
description: "Next.js configuration",
},
"next-env.d.ts": {
purpose: "types",
description: "Next.js TypeScript declarations",
},
"tsconfig.json": {
purpose: "config",
description: "TypeScript configuration",
},
"package.json": {
purpose: "config",
description: "Project dependencies and scripts",
},
},
conventions: [
"Use file-system based routing",
"Keep API routes in pages/api",
"Use CSS Modules for component styles",
"Follow Next.js data fetching patterns",
],
dependencies: {
next: "^14.1.0",
react: "^18.2.0",
"react-dom": "18.2.0",
tailwindcss: "^3.4.1",
},
scripts: {
dev: "next dev",
build: "next build",
start: "next start",
lint: "next lint",
},
},
// Streamlit template config
streamlit: {
id: "streamlit",
name: "Streamlit",
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
fileStructure: {
"main.py": {
purpose: "entry",
description: "Main Streamlit application file",
},
"requirements.txt": {
purpose: "dependencies",
description: "Python package dependencies",
},
Procfile: {
purpose: "deployment",
description: "Deployment configuration for hosting platforms",
},
"venv/": {
purpose: "environment",
description: "Python virtual environment directory",
},
},
conventions: [
"Use Streamlit components for UI",
"Follow PEP 8 style guide",
"Keep dependencies in requirements.txt",
"Use virtual environment for isolation",
],
dependencies: {
streamlit: "^1.40.0",
altair: "^5.5.0",
},
scripts: {
start: "streamlit run main.py",
dev: "./venv/bin/streamlit run main.py --server.runOnSave true",
},
},
// HTML template config
vanillajs: {
id: "vanillajs",
name: "HTML/JS",
runCommand: "npm run dev",
fileStructure: {
"index.html": {
purpose: "entry",
description: "Main HTML entry point",
},
"style.css": {
purpose: "styles",
description: "Global CSS styles",
},
"script.js": {
purpose: "scripts",
description: "JavaScript application logic",
},
"package.json": {
purpose: "config",
description: "Project dependencies and scripts",
},
"package-lock.json": {
purpose: "config",
description: "Locked dependency versions",
},
"vite.config.js": {
purpose: "config",
description: "Vite bundler configuration",
},
},
conventions: [
"Use semantic HTML elements",
"Keep CSS modular and organized",
"Write clean, modular JavaScript",
"Follow modern ES6+ practices",
],
dependencies: {
vite: "^5.0.12",
},
scripts: {
dev: "vite",
build: "vite build",
preview: "vite preview",
},
},
// PHP template config
php: {
id: "php",
name: "PHP",
runCommand: "npx vite",
fileStructure: {
"index.php": {
purpose: "entry",
description: "Main PHP entry point",
},
"package.json": {
purpose: "config",
description: "Frontend dependencies and scripts",
},
"package-lock.json": {
purpose: "config",
description: "Locked dependency versions",
},
"vite.config.js": {
purpose: "config",
description: "Vite configuration for frontend assets",
},
"node_modules/": {
purpose: "dependencies",
description: "Frontend dependency files",
},
},
conventions: [
"Follow PSR-12 coding standards",
"Use modern PHP 8+ features",
"Organize assets with Vite",
"Keep PHP logic separate from presentation",
],
dependencies: {
vite: "^5.0.0",
},
scripts: {
dev: "vite",
build: "vite build",
preview: "vite preview",
},
},
} }

View File

@ -1,19 +1,19 @@
export const TIERS = { export const TIERS = {
FREE: { FREE: {
// generations: 100, // generations: 100,
// maxTokens: 1024, // maxTokens: 1024,
generations: 1000, generations: 1000,
maxTokens: 4096, maxTokens: 4096,
model: "claude-3-5-sonnet-20240620", model: "claude-3-5-sonnet-20240620",
}, },
PRO: { PRO: {
generations: 500, generations: 500,
maxTokens: 2048, maxTokens: 2048,
model: "claude-3-5-sonnet-20240620", model: "claude-3-5-sonnet-20240620",
}, },
ENTERPRISE: { ENTERPRISE: {
generations: 1000, generations: 1000,
maxTokens: 4096, maxTokens: 4096,
model: "claude-3-5-sonnet-20240620", model: "claude-3-5-sonnet-20240620",
}, },
} }

View File

@ -1,7 +1,5 @@
// DB Types // DB Types
import { KNOWN_PLATFORMS } from "./constants"
export type User = { export type User = {
id: string id: string
name: string name: string
@ -10,20 +8,11 @@ export type User = {
avatarUrl: string | null avatarUrl: string | null
createdAt: Date createdAt: Date
generations: number generations: number
bio: string | null
personalWebsite: string | null
links: UserLink[]
tier: "FREE" | "PRO" | "ENTERPRISE"
tierExpiresAt: Date
lastResetDate: number
sandbox: Sandbox[] sandbox: Sandbox[]
usersToSandboxes: UsersToSandboxes[] usersToSandboxes: UsersToSandboxes[]
} tier: "FREE" | "PRO" | "ENTERPRISE"
tierExpiresAt: Date
export type KnownPlatform = (typeof KNOWN_PLATFORMS)[number] lastResetDate?: number
export type UserLink = {
url: string
platform: KnownPlatform
} }
export type Sandbox = { export type Sandbox = {
@ -33,13 +22,9 @@ export type Sandbox = {
visibility: "public" | "private" visibility: "public" | "private"
createdAt: Date createdAt: Date
userId: string userId: string
likeCount: number
viewCount: number
usersToSandboxes: UsersToSandboxes[] usersToSandboxes: UsersToSandboxes[]
} }
export type SandboxWithLiked = Sandbox & {
liked: boolean
}
export type UsersToSandboxes = { export type UsersToSandboxes = {
userId: string userId: string
sandboxId: string sandboxId: string

View File

@ -2,7 +2,7 @@ import { type ClassValue, clsx } from "clsx"
// import { toast } from "sonner" // import { toast } from "sonner"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import fileExtToLang from "./file-extension-to-language.json" import fileExtToLang from "./file-extension-to-language.json"
import { KnownPlatform, TFile, TFolder, UserLink } from "./types" import { TFile, TFolder } from "./types"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@ -98,57 +98,3 @@ export function sortFileExplorer(
return item return item
}) })
} }
export function parseSocialLink(url: string): UserLink {
try {
// Handle empty or invalid URLs
if (!url) return { url: "", platform: "generic" }
// Remove protocol and www prefix for consistent parsing
const cleanUrl = url
.toLowerCase()
.replace(/^https?:\/\//, "")
.replace(/^www\./, "")
.split("/")[0] // Get just the domain part
// Platform detection mapping
const platformPatterns: Record<
Exclude<KnownPlatform, "generic">,
RegExp
> = {
github: /github\.com/,
twitter: /(?:twitter\.com|x\.com|t\.co)/,
instagram: /instagram\.com/,
bluesky: /(?:bsky\.app|bluesky\.social)/,
linkedin: /linkedin\.com/,
youtube: /(?:youtube\.com|youtu\.be)/,
twitch: /twitch\.tv/,
discord: /discord\.(?:gg|com)/,
mastodon: /mastodon\.(?:social|online|world)/,
threads: /threads\.net/,
gitlab: /gitlab\.com/,
}
// Check URL against each pattern
for (const [platform, pattern] of Object.entries(platformPatterns)) {
if (pattern.test(cleanUrl)) {
return {
url,
platform: platform as KnownPlatform,
}
}
}
// Fall back to generic if no match found
return {
url,
platform: "generic",
}
} catch (error) {
console.error("Error parsing social link:", error)
return {
url: url || "",
platform: "generic",
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@
"@clerk/nextjs": "^4.29.12", "@clerk/nextjs": "^4.29.12",
"@clerk/themes": "^1.7.12", "@clerk/themes": "^1.7.12",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.3.4",
"@liveblocks/client": "^1.12.0", "@liveblocks/client": "^1.12.0",
"@liveblocks/node": "^1.12.0", "@liveblocks/node": "^1.12.0",
"@liveblocks/react": "^1.12.0", "@liveblocks/react": "^1.12.0",
@ -22,21 +22,17 @@
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.1.2",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.1", "@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-progress": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.6",
"@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",
@ -55,12 +51,11 @@
"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",
"next-themes": "^0.4.4", "next-themes": "^0.3.0",
"openai": "^4.73.1",
"posthog-js": "^1.147.0", "posthog-js": "^1.147.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.51.3",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-resizable-panels": "^2.0.16", "react-resizable-panels": "^2.0.16",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
@ -75,7 +70,7 @@
"y-monaco": "^0.1.5", "y-monaco": "^0.1.5",
"y-protocols": "^1.0.6", "y-protocols": "^1.0.6",
"yjs": "^13.6.15", "yjs": "^13.6.15",
"zod": "^3.24.1" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="14" fill="#8892BF"/>
<path d="M14.4392 10H16.1192L15.6444 12.5242H17.154C17.9819 12.5419 18.5986 12.7269 19.0045 13.0793C19.4184 13.4316 19.5402 14.1014 19.3698 15.0881L18.5541 19.4889H16.8497L17.6288 15.2863C17.7099 14.8457 17.6856 14.533 17.5558 14.348C17.426 14.163 17.146 14.0705 16.7158 14.0705L15.3644 14.0573L14.3661 19.4889H12.6861L14.4392 10Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.74092 12.5243H10.0036C10.9612 12.533 11.6552 12.8327 12.0854 13.4229C12.5156 14.0132 12.6576 14.8193 12.5115 15.8414C12.4548 16.3085 12.3289 16.7665 12.1341 17.2159C11.9474 17.6652 11.6878 18.0704 11.355 18.4317C10.9491 18.8898 10.5149 19.1805 10.0523 19.304C9.58969 19.4274 9.11076 19.489 8.61575 19.489H7.15484L6.69222 22H5L6.74092 12.5243ZM7.43485 17.9956L8.16287 14.0441H8.40879C8.49815 14.0441 8.5914 14.0396 8.6888 14.0309C9.33817 14.0221 9.87774 14.0882 10.308 14.2291C10.7462 14.37 10.8923 14.9031 10.7462 15.8282C10.5678 16.9296 10.2186 17.5727 9.69926 17.7577C9.1799 17.934 8.53053 18.0176 7.75138 18.0088H7.58094C7.53224 18.0088 7.48355 18.0043 7.43485 17.9956Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4365 12.5243H21.1738L19.4329 22H21.1251L21.5878 19.489H23.0487C23.5437 19.489 24.0226 19.4274 24.4852 19.304C24.9479 19.1805 25.382 18.8898 25.7879 18.4317C26.1207 18.0704 26.3803 17.6652 26.567 17.2159C26.7618 16.7665 26.8877 16.3085 26.9444 15.8414C27.0905 14.8193 26.9486 14.0132 26.5183 13.4229C26.0881 12.8327 25.3942 12.533 24.4365 12.5243ZM22.5958 14.0441L21.8678 17.9956C21.9165 18.0043 21.9652 18.0088 22.0139 18.0088H22.1843C22.9635 18.0176 23.6128 17.934 24.1322 17.7577C24.6515 17.5727 25.0007 16.9296 25.1792 15.8282C25.3253 14.9031 25.1792 14.37 24.7409 14.2291C24.3107 14.0882 23.7711 14.0221 23.1217 14.0309C23.0243 14.0396 22.9311 14.0441 22.8417 14.0441H22.5958Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB