Compare commits

..

1 Commits

Author SHA1 Message Date
James Murdza
a59246b1ef feat: correctly show whether a project has been deployed 2024-12-01 19:14:44 -08:00
57 changed files with 536 additions and 4288 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

@ -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:
@ -42,6 +42,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 +79,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:
``` ```
@ -176,15 +175,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,10 +183,6 @@ 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.
@ -262,7 +248,8 @@ backend/
├── database/ ├── database/
│ ├── src │ ├── src
│ └── drizzle │ └── drizzle
└── storage ├── storage
└── ai
``` ```
| Path | Description | | Path | Description |

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": "1288b006-6410-4b1c-8c96-d9797878a116",
"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",

View File

@ -1,353 +0,0 @@
{
"version": "5",
"dialect": "sqlite",
"id": "80c0b0b2-bb0e-449a-b447-c21863686f58",
"prevId": "4ada398d-7e4e-448f-8cea-a10b4d844600",
"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
}
},
"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

@ -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,8 @@
{ {
"idx": 0, "idx": 0,
"version": "5", "version": "5",
"when": 1736155854410, "when": 1732568535771,
"tag": "0000_sudden_wallop", "tag": "0000_rapid_korath",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1736169498666,
"tag": "0001_dusty_komodo",
"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,7 +5,13 @@ 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,
sandbox,
sandboxLikes,
user,
usersToSandboxes,
} from "./schema"
export interface Env { export interface Env {
DB: D1Database DB: D1Database
@ -67,7 +73,6 @@ export default {
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))
@ -240,88 +245,6 @@ export default {
return success return success
} else return methodNotAllowed } else return methodNotAllowed
} else if (path === "/api/sandbox/like") {
if (method === "POST") {
const likeSchema = z.object({
sandboxId: z.string(),
userId: z.string(),
})
try {
const body = await request.json()
const { sandboxId, userId } = likeSchema.parse(body)
// Check if user has already liked
const existingLike = await db.query.sandboxLikes.findFirst({
where: (likes, { and, eq }) =>
and(eq(likes.sandboxId, sandboxId), eq(likes.userId, userId)),
})
if (existingLike) {
// Unlike
await db
.delete(sandboxLikes)
.where(
and(
eq(sandboxLikes.sandboxId, sandboxId),
eq(sandboxLikes.userId, userId)
)
)
await db
.update(sandbox)
.set({
likeCount: sql`${sandbox.likeCount} - 1`,
})
.where(eq(sandbox.id, sandboxId))
return json({
message: "Unlike successful",
liked: false,
})
} else {
// Like
await db.insert(sandboxLikes).values({
sandboxId,
userId,
createdAt: new Date(),
})
await db
.update(sandbox)
.set({
likeCount: sql`${sandbox.likeCount} + 1`,
})
.where(eq(sandbox.id, sandboxId))
return json({
message: "Like successful",
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) {
return invalidRequest
}
const like = await db.query.sandboxLikes.findFirst({
where: (likes, { and, eq }) =>
and(eq(likes.sandboxId, sandboxId), eq(likes.userId, userId)),
})
return json({
liked: !!like,
})
} else {
return methodNotAllowed
}
} else if (path === "/api/user") { } else if (path === "/api/user") {
if (method === "GET") { if (method === "GET") {
const params = url.searchParams const params = url.searchParams
@ -393,7 +316,6 @@ export default {
email: z.string().email(), email: z.string().email(),
username: z.string(), username: z.string(),
avatarUrl: z.string().optional(), avatarUrl: z.string().optional(),
githubToken: z.string().nullable().optional(),
createdAt: z.string().optional(), createdAt: z.string().optional(),
generations: z.number().optional(), generations: z.number().optional(),
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]).optional(), tier: z.enum(["FREE", "PRO", "ENTERPRISE"]).optional(),
@ -409,7 +331,6 @@ export default {
email, email,
username, username,
avatarUrl, avatarUrl,
githubToken,
createdAt, createdAt,
generations, generations,
tier, tier,
@ -424,7 +345,6 @@ export default {
email, email,
username, username,
avatarUrl, avatarUrl,
githubToken,
createdAt: createdAt ? new Date(createdAt) : new Date(), createdAt: createdAt ? new Date(createdAt) : new Date(),
generations, generations,
tier, tier,
@ -445,20 +365,9 @@ export default {
const updateUserSchema = z.object({ const updateUserSchema = z.object({
id: z.string(), id: z.string(),
name: z.string().optional(), 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(), email: z.string().email().optional(),
username: z.string().optional(), username: z.string().optional(),
avatarUrl: z.string().optional(), avatarUrl: z.string().optional(),
githubToken: z.string().nullable().optional(),
generations: z.number().optional(), generations: z.number().optional(),
}) })

View File

@ -2,26 +2,6 @@ import { createId } from "@paralleldrive/cuid2"
import { relations, sql } from "drizzle-orm" import { relations, sql } from "drizzle-orm"
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core" import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"
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 // #region Tables
export const user = sqliteTable("user", { export const user = sqliteTable("user", {
id: text("id") id: text("id")
@ -32,14 +12,10 @@ export const user = sqliteTable("user", {
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" }).default( createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
sql`CURRENT_TIMESTAMP` sql`CURRENT_TIMESTAMP`
), ),
generations: integer("generations").default(0), generations: integer("generations").default(0),
bio: text("bio"),
personalWebsite: text("personalWebsite"),
links: text("links", { mode: "json" }).default("[]").$type<UserLink[]>(),
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"), tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
tierExpiresAt: integer("tierExpiresAt"), tierExpiresAt: integer("tierExpiresAt"),
lastResetDate: integer("lastResetDate"), lastResetDate: integer("lastResetDate"),
@ -63,7 +39,6 @@ export const sandbox = sqliteTable("sandbox", {
.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

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

@ -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
@ -326,69 +322,25 @@ export class FileManager {
// Save file content // Save file content
async saveFile(fileId: string, body: string): Promise<void> { async saveFile(fileId: string, body: string): Promise<void> {
if (!fileId) return // handles saving when no file is open if (!fileId) return // handles saving when no file is open
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,
} }
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

@ -170,20 +170,8 @@ export class Sandbox {
// Handle checking if an app exists // Handle checking if an app exists
const handleAppExists: SocketHandler = async ({ appName }) => { const handleAppExists: SocketHandler = async ({ appName }) => {
if (!this.dokkuClient) { if (!this.dokkuClient)
console.log("Failed to check app existence: No Dokku client") throw new Error("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 { return {
success: true, success: true,
exists: await this.dokkuClient.appExists(appName), exists: await this.dokkuClient.appExists(appName),

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,22 +1,16 @@
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"
import { TFile, TFolder } from "@/lib/types"
import { ignoredFiles, ignoredFolders } from "@/components/editor/AIChat/lib/ignored-paths"
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 // Format file structure for context
function formatFileStructure( function formatFileStructure(items: (TFile | TFolder)[] | undefined, prefix = ""): string {
items: (TFile | TFolder)[] | undefined,
prefix = ""
): string {
if (!items || !Array.isArray(items)) { if (!items || !Array.isArray(items)) {
return "No files available" return "No files available"
} }
@ -29,23 +23,15 @@ function formatFileStructure(
return sortedItems return sortedItems
.map((item) => { .map((item) => {
if ( if (item.type === "file" && !ignoredFiles.some(
item.type === "file" && (pattern) => item.name.endsWith(pattern.replace("*", "")) || item.name === pattern
!ignoredFiles.some( )) {
(pattern) =>
item.name.endsWith(pattern.replace("*", "")) ||
item.name === pattern
)
) {
return `${prefix}├── ${item.name}` return `${prefix}├── ${item.name}`
} else if ( } else if (
item.type === "folder" && item.type === "folder" &&
!ignoredFolders.some((folder) => folder === item.name) !ignoredFolders.some((folder) => folder === item.name)
) { ) {
const folderContent = formatFileStructure( const folderContent = formatFileStructure(item.children, `${prefix}`)
item.children,
`${prefix}`
)
return `${prefix}├── ${item.name}/\n${folderContent}` return `${prefix}├── ${item.name}/\n${folderContent}`
} }
return null return null
@ -108,7 +94,6 @@ export async function POST(request: Request) {
line, line,
templateType, templateType,
files, files,
projectName,
} = await request.json() } = await request.json()
// Get template configuration // Get template configuration
@ -153,23 +138,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}

View File

@ -19,9 +19,7 @@ export async function POST(request: Request) {
- Explanations or comments - Explanations or comments
- Markdown formatting - Markdown formatting
The output should be the exact code that will replace the existing code, nothing more and nothing less. 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 mergedCode = `Original file (${fileName}):\n${originalCode}\n\nNew code to merge:\n${newCode}`

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;

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

@ -9,19 +9,14 @@ 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=" py-2 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

@ -240,7 +240,7 @@ function ProjectCardComponent({
> >
<CanvasRevealEffect <CanvasRevealEffect
animationSpeed={3} animationSpeed={3}
containerClassName="bg-muted" containerClassName="bg-black"
colors={colors[type]} colors={colors[type]}
dotSize={2} dotSize={2}
/> />

View File

@ -8,7 +8,7 @@ interface ApplyButtonProps {
activeFileName: string activeFileName: string
activeFileContent: string activeFileContent: string
editorRef: { current: any } editorRef: { current: any }
onApply: (mergedCode: string, originalCode: string) => void onApply: (mergedCode: string) => void
} }
export default function ApplyButton({ export default function ApplyButton({
@ -48,7 +48,7 @@ export default function ApplyButton({
mergedCode += decoder.decode(value, { stream: true }) mergedCode += decoder.decode(value, { stream: true })
} }
} }
onApply(mergedCode.trim(), activeFileContent) onApply(mergedCode.trim())
} catch (error) { } catch (error) {
console.error("Error applying code:", error) console.error("Error applying code:", error)
toast.error( toast.error(

View File

@ -19,7 +19,6 @@ export default function ChatMessage({
editorRef, editorRef,
mergeDecorationsCollection, mergeDecorationsCollection,
setMergeDecorationsCollection, setMergeDecorationsCollection,
selectFile,
}: MessageProps) { }: MessageProps) {
// State for expanded message index // State for expanded message index
const [expandedMessageIndex, setExpandedMessageIndex] = useState< const [expandedMessageIndex, setExpandedMessageIndex] = useState<
@ -116,9 +115,8 @@ export default function ChatMessage({
activeFileContent, activeFileContent,
editorRef, editorRef,
handleApplyCode, handleApplyCode,
selectFile,
mergeDecorationsCollection, mergeDecorationsCollection,
setMergeDecorationsCollection, setMergeDecorationsCollection
) )
return ( return (
@ -126,8 +124,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 +169,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",

View File

@ -1,4 +1,3 @@
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 { ChevronDown, X } from "lucide-react"
@ -20,10 +19,8 @@ export default function AIChat({
files, files,
templateType, templateType,
handleApplyCode, handleApplyCode,
selectFile,
mergeDecorationsCollection, mergeDecorationsCollection,
setMergeDecorationsCollection, setMergeDecorationsCollection,
projectName,
}: AIChatProps) { }: AIChatProps) {
// Initialize socket and messages // Initialize socket and messages
const { socket } = useSocket() const { socket } = useSocket()
@ -155,8 +152,7 @@ export default function AIChat({
activeFileContent, activeFileContent,
false, false,
templateType, templateType,
files, files
projectName
) )
} }
@ -209,9 +205,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
@ -227,7 +223,6 @@ export default function AIChat({
editorRef={editorRef} editorRef={editorRef}
mergeDecorationsCollection={mergeDecorationsCollection} mergeDecorationsCollection={mergeDecorationsCollection}
setMergeDecorationsCollection={setMergeDecorationsCollection} setMergeDecorationsCollection={setMergeDecorationsCollection}
selectFile={selectFile}
/> />
))} ))}
{isLoading && <LoadingDots />} {isLoading && <LoadingDots />}
@ -242,7 +237,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,4 @@
import { TFile, TFolder } from "@/lib/types" import { TFolder, TFile } from "@/lib/types"
import React from "react" import React from "react"
// Stringify content for chat message component // Stringify content for chat message component
@ -93,8 +93,7 @@ export const handleSend = async (
activeFileContent: string, activeFileContent: string,
isEditMode: boolean = false, isEditMode: boolean = false,
templateType: string, templateType: string,
files: (TFile | TFolder)[], 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
@ -145,8 +144,7 @@ export const handleSend = async (
activeFileContent: activeFileContent, activeFileContent: activeFileContent,
isEditMode: isEditMode, isEditMode: isEditMode,
templateType: templateType, templateType: templateType,
files: files, files: files,
projectName: projectName,
}), }),
signal: abortControllerRef.current.signal, signal: abortControllerRef.current.signal,
}) })
@ -243,11 +241,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,13 +1,11 @@
import { useSocket } from "@/context/SocketContext" import { Check, CornerUpLeft, X } from "lucide-react"
import { TTab } from "@/lib/types"
import { Check, CornerUpLeft, FileText, X } from "lucide-react"
import monaco from "monaco-editor" 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 ApplyButton from "../ApplyButton"
import { isFilePath, stringifyContent } from "./chatUtils" import { stringifyContent } from "./chatUtils"
// Create markdown components for chat message component // Create markdown components for chat message component
export const createMarkdownComponents = ( export const createMarkdownComponents = (
@ -17,8 +15,7 @@ export const createMarkdownComponents = (
activeFileName: string, activeFileName: string,
activeFileContent: string, activeFileContent: string,
editorRef: any, editorRef: any,
handleApplyCode: (mergedCode: string, originalCode: string) => void, handleApplyCode: (mergedCode: string) => void,
selectFile: (tab: TTab) => void,
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection, mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection,
setMergeDecorationsCollection?: (collection: undefined) => void setMergeDecorationsCollection?: (collection: undefined) => void
): Components => ({ ): Components => ({
@ -61,7 +58,7 @@ export const createMarkdownComponents = (
mergeDecorationsCollection && mergeDecorationsCollection &&
editorRef?.current editorRef?.current
) { ) {
mergeDecorationsCollection?.clear() mergeDecorationsCollection.clear()
setMergeDecorationsCollection(undefined) setMergeDecorationsCollection(undefined)
} }
}} }}
@ -75,15 +72,14 @@ export const createMarkdownComponents = (
<div className="w-px bg-input"></div> <div className="w-px bg-input"></div>
<Button <Button
onClick={() => { onClick={() => {
if (editorRef?.current && mergeDecorationsCollection) { if (
const model = editorRef.current.getModel() setMergeDecorationsCollection &&
if (model && (model as any).originalContent) { mergeDecorationsCollection &&
editorRef.current?.setValue( editorRef?.current
(model as any).originalContent ) {
) editorRef.current.getModel()?.setValue(activeFileContent)
mergeDecorationsCollection.clear() mergeDecorationsCollection.clear()
setMergeDecorationsCollection?.(undefined) setMergeDecorationsCollection(undefined)
}
} }
}} }}
size="sm" size="sm"
@ -130,62 +126,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,9 @@ export interface AIChatProps {
files: (TFile | TFolder)[] files: (TFile | TFolder)[]
templateType: string templateType: string
templateConfig?: TemplateConfig templateConfig?: TemplateConfig
projectName: string handleApplyCode: (mergedCode: string) => void
handleApplyCode: (mergedCode: string, originalCode: string) => void
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
setMergeDecorationsCollection?: (collection: undefined) => void setMergeDecorationsCollection?: (collection: undefined) => void
selectFile: (tab: TTab) => void
} }
// Chat input props interface // Chat input props interface
@ -110,13 +108,12 @@ 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 handleApplyCode: (mergedCode: string) => void
activeFileName: string activeFileName: string
activeFileContent: string activeFileContent: string
editorRef: any editorRef: any
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
setMergeDecorationsCollection?: (collection: undefined) => void setMergeDecorationsCollection?: (collection: undefined) => void
selectFile: (tab: TTab) => void
} }
// Context tabs props interface // Context tabs props interface

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) {
@ -384,19 +384,12 @@ export default function CodeEditor({
// handle apply code // handle apply code
const handleApplyCode = useCallback( const handleApplyCode = useCallback(
(mergedCode: string, originalCode: string) => { (mergedCode: string) => {
if (!editorRef) return if (!editorRef) return
const model = editorRef.getModel() const originalLines = activeFileContent.split("\n")
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 mergedLines = mergedCode.split("\n")
const decorations: monaco.editor.IModelDeltaDecoration[] = [] const decorations: monaco.editor.IModelDeltaDecoration[] = []
for ( for (
@ -404,33 +397,32 @@ export default function CodeEditor({
i < Math.max(originalLines.length, mergedLines.length); i < Math.max(originalLines.length, mergedLines.length);
i++ i++
) { ) {
// Only highlight new lines (green highlights) if (originalLines[i] !== mergedLines[i]) {
if (i >= originalLines.length || originalLines[i] !== mergedLines[i]) {
decorations.push({ decorations.push({
range: new monaco.Range(i + 1, 1, i + 1, 1), range: new monaco.Range(i + 1, 1, i + 1, 1),
options: { options: {
isWholeLine: true, isWholeLine: true,
className: "added-line-decoration", className:
glyphMarginClassName: "added-line-glyph", i < originalLines.length
? "removed-line-decoration"
: "added-line-decoration",
glyphMarginClassName:
i < originalLines.length
? "removed-line-glyph"
: "added-line-glyph",
}, },
}) })
} }
} }
// Execute the edit operation // Update editor content
editorRef.executeEdits("apply-code", [ editorRef.setValue(mergedCode)
{
range: fullRange,
text: mergedCode,
forceMoveMarkers: true,
},
])
// Apply decorations after the edit // Apply decorations
const newDecorations = editorRef.createDecorationsCollection(decorations) const newDecorations = editorRef.createDecorationsCollection(decorations)
setMergeDecorationsCollection(newDecorations) setMergeDecorationsCollection(newDecorations)
}, },
[editorRef] [editorRef, activeFileContent]
) )
// Generate widget effect // Generate widget effect
@ -793,32 +785,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 +971,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>
@ -1293,18 +1284,16 @@ export default function CodeEditor({
lastCopiedRangeRef={lastCopiedRangeRef} lastCopiedRangeRef={lastCopiedRangeRef}
files={files} files={files}
templateType={sandboxData.type} templateType={sandboxData.type}
projectName={sandboxData.name}
handleApplyCode={handleApplyCode} handleApplyCode={handleApplyCode}
mergeDecorationsCollection={mergeDecorationsCollection} mergeDecorationsCollection={mergeDecorationsCollection}
setMergeDecorationsCollection={setMergeDecorationsCollection} setMergeDecorationsCollection={setMergeDecorationsCollection}
selectFile={selectFile}
/> />
</ResizablePanel> </ResizablePanel>
</> </>
)} )}
</ResizablePanelGroup> </ResizablePanelGroup>
</PreviewProvider> </PreviewProvider>
</div> </>
) )
} }

View File

@ -21,14 +21,12 @@ export default function DeployButtonModal({
const { deploy, getAppExists } = useTerminal() const { deploy, getAppExists } = useTerminal()
const [isDeploying, setIsDeploying] = useState(false) const [isDeploying, setIsDeploying] = useState(false)
const [isDeployed, setIsDeployed] = useState(false) const [isDeployed, setIsDeployed] = useState(false)
const [deployButtonVisible, setDeployButtonEnabled] = useState(false)
useEffect(() => { useEffect(() => {
const checkDeployment = async () => { const checkDeployment = async () => {
if (getAppExists) { if (getAppExists) {
const exists = await getAppExists(data.id) const exists = await getAppExists(data.id)
setDeployButtonEnabled(exists.success) setIsDeployed(exists)
setIsDeployed((exists.success && exists.exists) ?? false)
} }
} }
checkDeployment() checkDeployment()
@ -52,12 +50,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"

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

@ -22,7 +22,7 @@ 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)

View File

@ -9,38 +9,19 @@ import {
CardDescription, CardDescription,
CardTitle, CardTitle,
} from "@/components/ui/card" } from "@/components/ui/card"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { import {
HoverCard, HoverCard,
HoverCardContent, HoverCardContent,
HoverCardTrigger, HoverCardTrigger,
} from "@/components/ui/hover-card" } from "@/components/ui/hover-card"
import { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 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 { deleteSandbox, updateSandbox, updateUser } from "@/lib/actions"
import { socialIcons } from "@/lib/data"
import { editUserSchema, EditUserSchema } from "@/lib/schema"
import { TIERS } from "@/lib/tiers" import { TIERS } from "@/lib/tiers"
import { SandboxWithLiked, User, UserLink } from "@/lib/types" import { SandboxWithLiked, User } from "@/lib/types"
import { cn, parseSocialLink } from "@/lib/utils"
import { useUser } from "@clerk/nextjs" import { useUser } from "@clerk/nextjs"
import { zodResolver } from "@hookform/resolvers/zod"
import { import {
Edit, Edit,
Globe,
Heart, Heart,
Info, Info,
Loader2, Loader2,
@ -48,27 +29,17 @@ import {
Package2, Package2,
PlusCircle, PlusCircle,
Sparkles, Sparkles,
Trash2,
X, X,
} from "lucide-react" } from "lucide-react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { import { Fragment, useCallback, useEffect, useMemo, useState } from "react"
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState,
useTransition,
} from "react"
import { useFormState, useFormStatus } from "react-dom" import { useFormState, useFormStatus } from "react-dom"
import { useFieldArray, useForm } from "react-hook-form"
import { toast } from "sonner" import { toast } from "sonner"
import Avatar from "../ui/avatar" import Avatar from "../ui/avatar"
import { Badge } from "../ui/badge" import { Badge } from "../ui/badge"
import { Input } from "../ui/input" import { Input } from "../ui/input"
import { Progress } from "../ui/progress" import { Progress } from "../ui/progress"
import { Textarea } from "../ui/textarea"
// #region Profile Page // #region Profile Page
export default function ProfilePage({ export default function ProfilePage({
publicSandboxes, publicSandboxes,
@ -104,9 +75,6 @@ export default function ProfilePage({
generations={isOwnProfile ? loggedInUser.generations : undefined} generations={isOwnProfile ? loggedInUser.generations : undefined}
isOwnProfile={isOwnProfile} isOwnProfile={isOwnProfile}
tier={profileOwner.tier} tier={profileOwner.tier}
bio={profileOwner.bio}
personalWebsite={profileOwner.personalWebsite}
socialLinks={profileOwner.links}
/> />
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
@ -133,17 +101,11 @@ function ProfileCard({
joinedDate, joinedDate,
generations, generations,
isOwnProfile, isOwnProfile,
bio,
personalWebsite,
socialLinks = [],
tier, tier,
}: { }: {
name: string name: string
username: string username: string
avatarUrl: string | null avatarUrl: string | null
bio: string | null
personalWebsite: string | null
socialLinks: UserLink[]
sandboxes: SandboxWithLiked[] sandboxes: SandboxWithLiked[]
joinedDate: Date joinedDate: Date
generations?: number generations?: number
@ -151,8 +113,9 @@ function ProfileCard({
tier: string tier: string
}) { }) {
const { user } = useUser() const { user } = useUser()
const router = useRouter()
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [formState, formAction] = useFormState(updateUser, {})
const joinedAt = useMemo(() => { const joinedAt = useMemo(() => {
const date = new Date(joinedDate).toLocaleDateString("en-US", { const date = new Date(joinedDate).toLocaleDateString("en-US", {
month: "long", month: "long",
@ -177,126 +140,103 @@ function ProfileCard({
} }
}, [sandboxes]) }, [sandboxes])
const showAddMoreInfoBanner = useMemo(() => { useEffect(() => {
return !bio && !personalWebsite && (socialLinks?.length ?? 0) === 0 if ("message" in formState) {
}, [personalWebsite, bio, socialLinks]) toast.success(formState.message as String)
toggleEdit()
if ("newRoute" in formState && typeof formState.newRoute === "string") {
router.replace(formState.newRoute)
}
}
if ("error" in formState) {
const error = formState.error
if (typeof error === "string") {
toast.error(error)
} else {
toast.error("An Error Occured")
}
}
}, [formState])
return ( return (
<Card className="mb-6 md:mb-0 sticky top-6"> <Card className="mb-6 md:mb-0 sticky top-6">
{isOwnProfile && ( {isOwnProfile && (
<div className="absolute top-2 right-2 flex flex-col gap-2"> <Button
<TooltipProvider> onClick={toggleEdit}
<Tooltip> aria-label={isEditing ? "close edit form" : "open edit form"}
<TooltipTrigger asChild> size="smIcon"
<Button variant="secondary"
onClick={toggleEdit} className="rounded-full absolute top-2 right-2"
aria-label={isEditing ? "close edit form" : "open edit form"} >
size="smIcon" {isEditing ? <X className="size-4" /> : <Edit className="size-4" />}
variant="secondary" </Button>
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"> <CardContent className="flex flex-col gap-4 items-center pt-6">
{isEditing ? ( <Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
<div className="flex flex-col gap-2 items-center ">
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" /> {!isEditing ? (
<EditProfileForm <div className="space-y-2">
{...{ <CardTitle className="text-2xl text-center">{name}</CardTitle>
name, <CardDescription className="text-center">{`@${username}`}</CardDescription>
username,
avatarUrl,
bio,
personalWebsite,
socialLinks,
toggleEdit,
}}
/>
</div> </div>
) : ( ) : (
<form action={formAction} className="flex flex-col gap-2">
<Input
name="id"
placeholder="ID"
className="hidden "
value={user?.id}
/>
<Input
name="oldUsername"
placeholder="ID"
className="hidden "
value={user?.username ?? undefined}
/>
<div className="space-y-1">
<Label htmlFor="input-name">Name</Label>
<Input
id="input-name"
name="name"
placeholder="Name"
defaultValue={name}
/>
</div>
<div className="space-y-1">
<Label htmlFor="input-username">User name</Label>
<div className="relative">
<Input
id="input-username"
className="peer ps-6"
type="text"
name="username"
placeholder="Username"
defaultValue={username}
/>
<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>
</div>
<SubmitButton />
</form>
)}
{!isEditing && (
<> <>
<div className="flex flex-col gap-2 items-center"> <div className="flex gap-6">
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" /> <StatsItem icon={Package2} label={stats.sandboxes} />
<div className="space-y-1"> <StatsItem icon={Heart} label={stats.likes} />
<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>
<div className="flex flex-col gap-1 items-center"> <div className="flex flex-col items-center gap-2">
<p className="text-xs text-muted-foreground">{joinedAt}</p>
{typeof generations === "number" && ( {typeof generations === "number" && (
<div className="flex justify-center"> <SubscriptionBadge
<SubscriptionBadge generations={generations}
generations={generations} tier={tier as keyof typeof TIERS}
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> </div>
<p className="text-xs mt-2 text-muted-foreground text-center">
{joinedAt}
</p>
</> </>
)} )}
</CardContent> </CardContent>
@ -304,213 +244,13 @@ function ProfileCard({
) )
} }
function EditProfileForm(props: { function SubmitButton() {
name: string const { pending } = useFormStatus()
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 ( return (
<Form {...form}> <Button size="sm" type="submit" className="w-full mt-2" disabled={pending}>
<form {pending && <Loader2 className="animate-spin mr-2 h-4 w-4" />}
ref={formRef} Save
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> </Button>
) )
} }
@ -701,8 +441,8 @@ interface StatsItemProps {
const StatsItem = ({ icon: Icon, label }: StatsItemProps) => ( const StatsItem = ({ icon: Icon, label }: StatsItemProps) => (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Icon size={16} /> <Icon size={18} />
<span className="text-sm text-muted-foreground">{label}</span> <span className="text-sm text-muted-foreground">{label}</span>
</div> </div>
) )
// #endregion // #endregion
@ -722,8 +462,8 @@ const SubscriptionBadge = ({
</Badge> </Badge>
<HoverCard> <HoverCard>
<HoverCardTrigger> <HoverCardTrigger>
<Button variant="ghost" size="smIcon" className="size-[26px]"> <Button variant="ghost" size="smIcon">
<Info size={16} /> <Info size={20} />
</Button> </Button>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent> <HoverCardContent>

View File

@ -9,19 +9,14 @@ import { Button } from "../ui/button"
export default function ProfileNavbar({ userData }: { userData: User }) { export default function ProfileNavbar({ userData }: { userData: User }) {
return ( return (
<nav className=" py-2 px-4 w-full flex items-center justify-between border-b border-border"> <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"> <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">
<ThemeSwitcher /> <ThemeSwitcher />

View File

@ -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,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,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

@ -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

@ -20,9 +20,7 @@ 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: getAppExists: ((appName: string) => Promise<boolean>) | null
| ((appName: string) => Promise<{ success: boolean; exists?: boolean }>)
| null
} }
const TerminalContext = createContext<TerminalContextType | undefined>( const TerminalContext = createContext<TerminalContextType | undefined>(
@ -94,18 +92,16 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
}) })
} }
const getAppExists = async ( const getAppExists = async (appName: string): Promise<boolean> => {
appName: string
): Promise<{ success: boolean; exists?: boolean }> => {
console.log("Is there a socket: " + !!socket) console.log("Is there a socket: " + !!socket)
if (!socket) { if (!socket) {
console.error("Couldn't check if app exists: No socket") console.error("Couldn't check if app exists: No socket")
return { success: false } return false
} }
const response: { success: boolean; exists?: boolean } = await new Promise( const response: { exists: boolean } = await new Promise((resolve) =>
(resolve) => socket.emit("getAppExists", { appName }, resolve) socket.emit("getAppExists", { appName }, resolve)
) )
return response return response.exists
} }
const value = { const value = {

View File

@ -2,9 +2,6 @@
import { revalidatePath } from "next/cache" import { revalidatePath } from "next/cache"
import { z } from "zod" 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
@ -97,7 +94,7 @@ export async function unshareSandbox(sandboxId: string, userId: string) {
} }
export async function toggleLike(sandboxId: string, userId: string) { export async function toggleLike(sandboxId: string, userId: string) {
const res = await fetch( await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/sandbox/like`, `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/sandbox/like`,
{ {
method: "POST", method: "POST",
@ -126,31 +123,20 @@ const UpdateErrorSchema = z.object({
.optional(), .optional(),
}) })
interface FormState { export async function updateUser(prevState: any, formData: FormData) {
message: string const data = Object.fromEntries(formData)
error?: any
newRoute?: string const schema = z.object({
fields?: Record<string, unknown> id: z.string(),
} username: z.string(),
export async function updateUser( oldUsername: z.string(),
prevState: any, name: z.string(),
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 console.log(data)
data.links = links
try { try {
const validatedData = editUserSchema.parse(data) const validatedData = schema.parse(data)
const changedUsername = validatedData.username !== validatedData.oldUsername const changedUsername = validatedData.username !== validatedData.oldUsername
const res = await fetch( const res = await fetch(
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`, `${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
@ -164,9 +150,6 @@ export async function updateUser(
id: validatedData.id, id: validatedData.id,
username: data.username ?? undefined, username: data.username ?? undefined,
name: data.name ?? undefined, name: data.name ?? undefined,
bio: data.bio ?? undefined,
personalWebsite: data.personalWebsite ?? undefined,
links: data.links ?? undefined,
}), }),
} }
) )
@ -177,11 +160,11 @@ export async function updateUser(
const parseResult = UpdateErrorSchema.safeParse(responseData) const parseResult = UpdateErrorSchema.safeParse(responseData)
if (!parseResult.success) { if (!parseResult.success) {
return { return { error: "Unexpected error occurred" }
message: "Unexpected error occurred", }
error: parseResult.error,
fields: validatedData, if (parseResult.data.error) {
} return parseResult.data
} }
if (changedUsername) { if (changedUsername) {
@ -192,13 +175,12 @@ export async function updateUser(
return { message: "Successfully updated" } return { message: "Successfully updated" }
} catch (error) { } catch (error) {
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
console.log(error)
return { return {
message: "Invalid data", error: error.errors?.[0].message,
error: error.errors,
fields: data,
} }
} }
return { message: "An unexpected error occurred", fields: data } return { error: "An unexpected error occurred" }
} }
} }

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

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,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 = {

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",
}
}
}

View File

@ -13,7 +13,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",
@ -27,15 +27,14 @@
"@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-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-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.1.3",
"@react-three/fiber": "^8.16.6", "@react-three/fiber": "^8.16.6",
"@uiw/codemirror-theme-vscode": "^4.23.5", "@uiw/codemirror-theme-vscode": "^4.23.5",
"@uiw/react-codemirror": "^4.23.5", "@uiw/react-codemirror": "^4.23.5",
@ -54,12 +53,12 @@
"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", "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",
@ -74,7 +73,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",
@ -910,9 +909,9 @@
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
}, },
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "3.9.1", "version": "3.3.4",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
"integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
"peerDependencies": { "peerDependencies": {
"react-hook-form": "^7.0.0" "react-hook-form": "^7.0.0"
} }
@ -1366,24 +1365,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": { "node_modules/@radix-ui/react-arrow": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
@ -1553,24 +1534,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": { "node_modules/@radix-ui/react-compose-refs": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
@ -1669,24 +1632,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": { "node_modules/@radix-ui/react-direction": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz",
@ -2171,39 +2116,18 @@
} }
}, },
"node_modules/@radix-ui/react-label": { "node_modules/@radix-ui/react-label": {
"version": "2.1.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz",
"integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==",
"dependencies": { "dependencies": {
"@radix-ui/react-primitive": "2.0.1" "@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.3"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
"@types/react-dom": "*", "@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" "react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@types/react": { "@types/react": {
@ -2254,24 +2178,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": { "node_modules/@radix-ui/react-popover": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz",
@ -2787,24 +2693,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress": { "node_modules/@radix-ui/react-progress": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz",
@ -2930,161 +2818,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.2.tgz",
"integrity": "sha512-EFI1N/S3YxZEW/lJ/H1jY3njlvTd8tBmgKEn4GHi51+aMm94i6NmAJstsm5cu3yJwYqYc93gpCPm21FeAbFk6g==",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.1",
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-presence": "1.1.2",
"@radix-ui/react-primitive": "2.0.1",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
"integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
"dependencies": {
"@radix-ui/react-slot": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": { "node_modules/@radix-ui/react-select": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
@ -3128,7 +2861,7 @@
} }
} }
}, },
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
@ -3146,37 +2879,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-slot": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch": { "node_modules/@radix-ui/react-switch": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.0.3.tgz",
@ -3490,22 +3192,23 @@
} }
}, },
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.1.6", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.6.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.3.tgz",
"integrity": "sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==", "integrity": "sha512-Z4w1FIS0BqVFI2c1jZvb/uDVJijJjJ2ZMuPV81oVgTZ7g3BZxobplnMVvXtFWgtozdvYJ+MFWtwkM5S2HnAong==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/primitive": "1.1.1", "@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1", "@radix-ui/react-context": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.3", "@radix-ui/react-dismissable-layer": "1.1.1",
"@radix-ui/react-id": "1.1.0", "@radix-ui/react-id": "1.1.0",
"@radix-ui/react-popper": "1.2.1", "@radix-ui/react-popper": "1.2.0",
"@radix-ui/react-portal": "1.1.3", "@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-presence": "1.1.2", "@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.1", "@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0",
"@radix-ui/react-visually-hidden": "1.1.1" "@radix-ui/react-visually-hidden": "1.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -3523,16 +3226,18 @@
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": {
"version": "1.1.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
"license": "MIT"
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
"version": "1.1.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
"integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-primitive": "2.0.1" "@radix-ui/react-primitive": "2.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -3550,9 +3255,10 @@
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
@ -3579,13 +3285,14 @@
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.3", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
"integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/primitive": "1.1.1", "@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-escape-keydown": "1.1.0" "@radix-ui/react-use-escape-keydown": "1.1.0"
}, },
@ -3623,15 +3330,16 @@
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
"version": "1.2.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
"integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/react-dom": "^2.0.0", "@floating-ui/react-dom": "^2.0.0",
"@radix-ui/react-arrow": "1.1.1", "@radix-ui/react-arrow": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1", "@radix-ui/react-context": "1.1.0",
"@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0",
"@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-rect": "1.1.0",
@ -3653,12 +3361,28 @@
} }
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
"version": "1.1.3", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
"integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-layout-effect": "1.1.0" "@radix-ui/react-use-layout-effect": "1.1.0"
}, },
"peerDependencies": { "peerDependencies": {
@ -3677,11 +3401,12 @@
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": {
"version": "1.1.2", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
"integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0" "@radix-ui/react-use-layout-effect": "1.1.0"
}, },
"peerDependencies": { "peerDependencies": {
@ -3700,11 +3425,12 @@
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
"version": "2.0.1", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
"integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-slot": "1.1.1" "@radix-ui/react-slot": "1.1.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -3721,6 +3447,24 @@
} }
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
@ -3758,6 +3502,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0" "@radix-ui/react-use-callback-ref": "1.1.0"
}, },
@ -3790,6 +3535,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/rect": "1.1.0" "@radix-ui/rect": "1.1.0"
}, },
@ -3807,6 +3553,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.0" "@radix-ui/react-use-layout-effect": "1.1.0"
}, },
@ -3821,11 +3568,12 @@
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
"version": "1.1.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
"integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-primitive": "2.0.1" "@radix-ui/react-primitive": "2.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -3845,7 +3593,8 @@
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
"license": "MIT"
}, },
"node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.1", "version": "1.0.1",
@ -7576,13 +7325,12 @@
} }
}, },
"node_modules/next-themes": { "node_modules/next-themes": {
"version": "0.4.4", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
"integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==", "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==",
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react": "^16.8 || ^17 || ^18",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" "react-dom": "^16.8 || ^17 || ^18"
} }
}, },
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
@ -8280,18 +8028,18 @@
} }
}, },
"node_modules/react-hook-form": { "node_modules/react-hook-form": {
"version": "7.54.2", "version": "7.51.3",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
"integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=12.22.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/react-hook-form" "url": "https://opencollective.com/react-hook-form"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19" "react": "^16.8.0 || ^17 || ^18"
} }
}, },
"node_modules/react-markdown": { "node_modules/react-markdown": {
@ -9976,9 +9724,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.24.1", "version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

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",
@ -28,15 +28,14 @@
"@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-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-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.1.3",
"@react-three/fiber": "^8.16.6", "@react-three/fiber": "^8.16.6",
"@uiw/codemirror-theme-vscode": "^4.23.5", "@uiw/codemirror-theme-vscode": "^4.23.5",
"@uiw/react-codemirror": "^4.23.5", "@uiw/react-codemirror": "^4.23.5",
@ -55,12 +54,12 @@
"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", "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 +74,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",