Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
46c715b1eb | ||
|
d87f818241 | ||
|
a25097108d |
1
.gitignore
vendored
1
.gitignore
vendored
@ -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
|
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -5,9 +5,5 @@
|
|||||||
"source.fixAll": "explicit",
|
"source.fixAll": "explicit",
|
||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": "explicit"
|
||||||
},
|
},
|
||||||
"tailwindCSS.experimental.classRegex": [
|
|
||||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
|
||||||
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
|
||||||
],
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
}
|
}
|
||||||
|
50
README.md
50
README.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
![2024-10-2307 17 42-ezgif com-resize](https://github.com/user-attachments/assets/a4057129-81a7-4a31-a093-c8bc8189ae72)
|
![2024-10-2307 17 42-ezgif com-resize](https://github.com/user-attachments/assets/a4057129-81a7-4a31-a093-c8bc8189ae72)
|
||||||
|
|
||||||
Sandbox is an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration, and AI chat.
|
Sandbox is an open-source cloud-based code editing environment with custom AI code generation, live preview, real-time collaboration and AI chat.
|
||||||
|
|
||||||
For the latest updates, join our Discord server: [discord.gitwit.dev](https://discord.gitwit.dev/).
|
For the latest updates, join our Discord server: [discord.gitwit.dev](https://discord.gitwit.dev/).
|
||||||
|
|
||||||
@ -10,11 +10,11 @@ For the latest updates, join our Discord server: [discord.gitwit.dev](https://di
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- Double-check that whatever you change "SUPERDUPERSECRET" to, it's the same in all config files.
|
- Double check that whatever you change "SUPERDUPERSECRET" to, it's the same in all config files.
|
||||||
|
|
||||||
### 0. Requirements
|
### 0. Requirements
|
||||||
|
|
||||||
The application uses NodeJS for the backend, NextJS for the frontend, and Cloudflare workers for additional backend tasks.
|
The application uses NodeJS for the backend, NextJS for the frontend and Cloudflare workers for additional backend tasks.
|
||||||
|
|
||||||
Needed accounts to set up:
|
Needed accounts to set up:
|
||||||
|
|
||||||
@ -22,7 +22,6 @@ Needed accounts to set up:
|
|||||||
- [Liveblocks](https://liveblocks.io/): Used for collaborative editing.
|
- [Liveblocks](https://liveblocks.io/): Used for collaborative editing.
|
||||||
- [E2B](https://e2b.dev/): Used for the terminals and live preview.
|
- [E2B](https://e2b.dev/): Used for the terminals and live preview.
|
||||||
- [Cloudflare](https://www.cloudflare.com/): Used for relational data storage (D2) and file storage (R2).
|
- [Cloudflare](https://www.cloudflare.com/): Used for relational data storage (D2) and file storage (R2).
|
||||||
- [Anthropic](https://anthropic.com/) and [OpenAI](https://openai.com/): API keys for code generation.
|
|
||||||
|
|
||||||
A quick overview of the tech before we start: The deployment uses a **NextJS** app for the frontend and an **ExpressJS** server on the backend. Presumably that's because NextJS integrates well with Clerk middleware but not with Socket.io.
|
A quick overview of the tech before we start: The deployment uses a **NextJS** app for the frontend and an **ExpressJS** server on the backend. Presumably that's because NextJS integrates well with Clerk middleware but not with Socket.io.
|
||||||
|
|
||||||
@ -42,6 +41,7 @@ Run `npm install` in:
|
|||||||
/backend/database
|
/backend/database
|
||||||
/backend/storage
|
/backend/storage
|
||||||
/backend/server
|
/backend/server
|
||||||
|
/backend/ai
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Adding Clerk
|
### 2. Adding Clerk
|
||||||
@ -78,8 +78,6 @@ npx wrangler deploy
|
|||||||
|
|
||||||
### 4. Deploying the database
|
### 4. Deploying the database
|
||||||
|
|
||||||
Follow this [guide](https://docs.google.com/document/d/1w5dA5daic_sIYB5Seni1KvnFx51pPV2so6lLdN2xa7Q/edit?usp=sharing) for more info.
|
|
||||||
|
|
||||||
Create a database:
|
Create a database:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -145,7 +143,21 @@ Update `/backend/server/.env`:
|
|||||||
E2B_API_KEY='🔑'
|
E2B_API_KEY='🔑'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9. Configuring the frontend
|
### 9. Adding AI code generation
|
||||||
|
|
||||||
|
In the `/backend/ai` directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `/backend/server/.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
AI_WORKER_URL='https://ai.🍎.workers.dev'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10. Configuring the frontend
|
||||||
|
|
||||||
Update `/frontend/.env`:
|
Update `/frontend/.env`:
|
||||||
|
|
||||||
@ -153,11 +165,9 @@ Update `/frontend/.env`:
|
|||||||
NEXT_PUBLIC_DATABASE_WORKER_URL='https://database.🍎.workers.dev'
|
NEXT_PUBLIC_DATABASE_WORKER_URL='https://database.🍎.workers.dev'
|
||||||
NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
|
NEXT_PUBLIC_STORAGE_WORKER_URL='https://storage.🍎.workers.dev'
|
||||||
NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET'
|
NEXT_PUBLIC_WORKERS_KEY='SUPERDUPERSECRET'
|
||||||
ANTHROPIC_API_KEY='🔑'
|
|
||||||
OPENAI_API_KEY='🔑'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 10. Running the IDE
|
### 11. Running the IDE
|
||||||
|
|
||||||
Run `npm run dev` simultaneously in:
|
Run `npm run dev` simultaneously in:
|
||||||
|
|
||||||
@ -176,15 +186,6 @@ Setting up deployments first requires a separate domain (such as gitwit.app, whi
|
|||||||
|
|
||||||
We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d
|
We then deploy Dokku on a separate server, according to this guide: https://dev.to/jamesmurdza/host-your-own-paas-platform-as-a-service-on-amazon-web-services-3f0d
|
||||||
|
|
||||||
And we install [dokku-daemon](https://github.com/dokku/dokku-daemon) with the following commands:
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/dokku/dokku-daemon
|
|
||||||
cd dokku-daemon
|
|
||||||
sudo make install
|
|
||||||
systemctl start dokku-daemon
|
|
||||||
```
|
|
||||||
|
|
||||||
The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env:
|
The Sandbox platform connects to the Dokku server via SSH, using SSH keys specifically generated for this connection. The SSH key is stored on the Sandbox server, and the following environment variables are set in /backend/server/.env:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -193,21 +194,16 @@ DOKKU_USERNAME=
|
|||||||
DOKKU_KEY=
|
DOKKU_KEY=
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploying to AWS
|
|
||||||
|
|
||||||
The backend server and deployments server can be deployed using AWS's EC2 service. See [our video guide](https://www.youtube.com/watch?v=WN8HQnimjmk) on how to do this.
|
|
||||||
|
|
||||||
## Creating Custom Templates
|
## Creating Custom Templates
|
||||||
|
|
||||||
Anyone can contribute a custom template for integration in Sandbox. Since Sandbox is built on E2B, there is no limitation to what langauge or runtime a Sandbox can use.
|
Anyone can contribute a custom template for integration in Sandbox. Since Sandbox is built on E2B, there is no limitation to what langauge or runtime a Sandbox can use.
|
||||||
|
|
||||||
Currently there are five templates:
|
Currently there are four templates:
|
||||||
|
|
||||||
- [jamesmurdza/dokku-reactjs-template](https://github.com/jamesmurdza/dokku-reactjs-template)
|
- [jamesmurdza/dokku-reactjs-template](https://github.com/jamesmurdza/dokku-reactjs-template)
|
||||||
- [jamesmurdza/dokku-vanillajs-template](https://github.com/jamesmurdza/dokku-vanillajs-template)
|
- [jamesmurdza/dokku-vanillajs-template](https://github.com/jamesmurdza/dokku-vanillajs-template)
|
||||||
- [jamesmurdza/dokku-nextjs-template](https://github.com/jamesmurdza/dokku-nextjs-template)
|
- [jamesmurdza/dokku-nextjs-template](https://github.com/jamesmurdza/dokku-nextjs-template)
|
||||||
- [jamesmurdza/dokku-streamlit-template](https://github.com/jamesmurdza/dokku-streamlit-template)
|
- [jamesmurdza/dokku-streamlit-template](https://github.com/jamesmurdza/dokku-streamlit-template)
|
||||||
- [omarrwd/dokku-php-template](https://github.com/omarrwd/dokku-php-template)
|
|
||||||
|
|
||||||
To create your own template, you can fork one of the above templates or start with a new blank repository. The template should have at least an `e2b.Dockerfile`, which is used by E2B to create the development environment. Optionally, a `Dockerfile` can be added which will be used to create the project build when it is deployed.
|
To create your own template, you can fork one of the above templates or start with a new blank repository. The template should have at least an `e2b.Dockerfile`, which is used by E2B to create the development environment. Optionally, a `Dockerfile` can be added which will be used to create the project build when it is deployed.
|
||||||
|
|
||||||
@ -262,7 +258,8 @@ backend/
|
|||||||
├── database/
|
├── database/
|
||||||
│ ├── src
|
│ ├── src
|
||||||
│ └── drizzle
|
│ └── drizzle
|
||||||
└── storage
|
├── storage
|
||||||
|
└── ai
|
||||||
```
|
```
|
||||||
|
|
||||||
| Path | Description |
|
| Path | Description |
|
||||||
@ -271,6 +268,7 @@ backend/
|
|||||||
| `backend/server` | The Express websocket server. |
|
| `backend/server` | The Express websocket server. |
|
||||||
| `backend/database` | API for interfacing with the D1 database (SQLite). |
|
| `backend/database` | API for interfacing with the D1 database (SQLite). |
|
||||||
| `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. |
|
| `backend/storage` | API for interfacing with R2 storage. Service-bound to `/backend/database`. |
|
||||||
|
| `backend/ai` | API for making requests to Workers AI . |
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
|
@ -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`);
|
|
@ -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 '[]';
|
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE sandbox ADD `containerId` text;
|
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "4ada398d-7e4e-448f-8cea-a10b4d844600",
|
"id": "3241d14f-687f-4134-94ab-88bf36e8611e",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"tables": {
|
"tables": {
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
@ -94,72 +94,6 @@
|
|||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {}
|
"uniqueConstraints": {}
|
||||||
},
|
},
|
||||||
"sandbox_likes": {
|
|
||||||
"name": "sandbox_likes",
|
|
||||||
"columns": {
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sandbox_id": {
|
|
||||||
"name": "sandbox_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"sandbox_likes_user_id_user_id_fk": {
|
|
||||||
"name": "sandbox_likes_user_id_user_id_fk",
|
|
||||||
"tableFrom": "sandbox_likes",
|
|
||||||
"tableTo": "user",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"sandbox_likes_sandbox_id_sandbox_id_fk": {
|
|
||||||
"name": "sandbox_likes_sandbox_id_sandbox_id_fk",
|
|
||||||
"tableFrom": "sandbox_likes",
|
|
||||||
"tableTo": "sandbox",
|
|
||||||
"columnsFrom": [
|
|
||||||
"sandbox_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sandbox_likes_sandbox_id_user_id_pk": {
|
|
||||||
"columns": [
|
|
||||||
"sandbox_id",
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"name": "sandbox_likes_sandbox_id_user_id_pk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"user": {
|
"user": {
|
||||||
"name": "user",
|
"name": "user",
|
||||||
"columns": {
|
"columns": {
|
||||||
@ -198,13 +132,6 @@
|
|||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
},
|
||||||
"githubToken": {
|
|
||||||
"name": "githubToken",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"name": "createdAt",
|
"name": "createdAt",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@ -220,28 +147,6 @@
|
|||||||
"notNull": false,
|
"notNull": false,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": 0
|
"default": 0
|
||||||
},
|
|
||||||
"tier": {
|
|
||||||
"name": "tier",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'FREE'"
|
|
||||||
},
|
|
||||||
"tierExpiresAt": {
|
|
||||||
"name": "tierExpiresAt",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lastResetDate": {
|
|
||||||
"name": "lastResetDate",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {
|
"indexes": {
|
||||||
|
@ -1,353 +1,258 @@
|
|||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "80c0b0b2-bb0e-449a-b447-c21863686f58",
|
"id": "7eafd85c-d63d-43be-aa26-1a0eb2ca1805",
|
||||||
"prevId": "4ada398d-7e4e-448f-8cea-a10b4d844600",
|
"prevId": "3241d14f-687f-4134-94ab-88bf36e8611e",
|
||||||
"tables": {
|
"tables": {
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"name": "sandbox",
|
"name": "sandbox",
|
||||||
"columns": {
|
"columns": {
|
||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false
|
"autoincrement": false
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"visibility": {
|
|
||||||
"name": "visibility",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"name": "user_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"likeCount": {
|
|
||||||
"name": "likeCount",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"viewCount": {
|
|
||||||
"name": "viewCount",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 0
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"indexes": {
|
"name": {
|
||||||
"sandbox_id_unique": {
|
"name": "name",
|
||||||
"name": "sandbox_id_unique",
|
"type": "text",
|
||||||
"columns": [
|
"primaryKey": false,
|
||||||
"id"
|
"notNull": true,
|
||||||
],
|
"autoincrement": false
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"foreignKeys": {
|
"type": {
|
||||||
"sandbox_user_id_user_id_fk": {
|
"name": "type",
|
||||||
"name": "sandbox_user_id_user_id_fk",
|
"type": "text",
|
||||||
"tableFrom": "sandbox",
|
"primaryKey": false,
|
||||||
"tableTo": "user",
|
"notNull": true,
|
||||||
"columnsFrom": [
|
"autoincrement": false
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"visibility": {
|
||||||
"uniqueConstraints": {}
|
"name": "visibility",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"likeCount": {
|
||||||
|
"name": "likeCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"viewCount": {
|
||||||
|
"name": "viewCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sandbox_likes": {
|
"indexes": {
|
||||||
"name": "sandbox_likes",
|
"sandbox_id_unique": {
|
||||||
"columns": {
|
"name": "sandbox_id_unique",
|
||||||
"user_id": {
|
"columns": [
|
||||||
"name": "user_id",
|
"id"
|
||||||
"type": "text",
|
],
|
||||||
"primaryKey": false,
|
"isUnique": true
|
||||||
"notNull": true,
|
}
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sandbox_id": {
|
|
||||||
"name": "sandbox_id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"sandbox_likes_user_id_user_id_fk": {
|
|
||||||
"name": "sandbox_likes_user_id_user_id_fk",
|
|
||||||
"tableFrom": "sandbox_likes",
|
|
||||||
"tableTo": "user",
|
|
||||||
"columnsFrom": [
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"sandbox_likes_sandbox_id_sandbox_id_fk": {
|
|
||||||
"name": "sandbox_likes_sandbox_id_sandbox_id_fk",
|
|
||||||
"tableFrom": "sandbox_likes",
|
|
||||||
"tableTo": "sandbox",
|
|
||||||
"columnsFrom": [
|
|
||||||
"sandbox_id"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sandbox_likes_sandbox_id_user_id_pk": {
|
|
||||||
"columns": [
|
|
||||||
"sandbox_id",
|
|
||||||
"user_id"
|
|
||||||
],
|
|
||||||
"name": "sandbox_likes_sandbox_id_user_id_pk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
},
|
||||||
"user": {
|
"foreignKeys": {
|
||||||
"name": "user",
|
"sandbox_user_id_user_id_fk": {
|
||||||
"columns": {
|
"name": "sandbox_user_id_user_id_fk",
|
||||||
"id": {
|
"tableFrom": "sandbox",
|
||||||
"name": "id",
|
"tableTo": "user",
|
||||||
"type": "text",
|
"columnsFrom": [
|
||||||
"primaryKey": true,
|
"user_id"
|
||||||
"notNull": true,
|
],
|
||||||
"autoincrement": false
|
"columnsTo": [
|
||||||
},
|
"id"
|
||||||
"name": {
|
],
|
||||||
"name": "name",
|
"onDelete": "no action",
|
||||||
"type": "text",
|
"onUpdate": "no action"
|
||||||
"primaryKey": false,
|
}
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"name": "username",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"avatarUrl": {
|
|
||||||
"name": "avatarUrl",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"githubToken": {
|
|
||||||
"name": "githubToken",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "CURRENT_TIMESTAMP"
|
|
||||||
},
|
|
||||||
"generations": {
|
|
||||||
"name": "generations",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 0
|
|
||||||
},
|
|
||||||
"bio": {
|
|
||||||
"name": "bio",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"personalWebsite": {
|
|
||||||
"name": "personalWebsite",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"links": {
|
|
||||||
"name": "links",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'[]'"
|
|
||||||
},
|
|
||||||
"tier": {
|
|
||||||
"name": "tier",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'FREE'"
|
|
||||||
},
|
|
||||||
"tierExpiresAt": {
|
|
||||||
"name": "tierExpiresAt",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lastResetDate": {
|
|
||||||
"name": "lastResetDate",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {
|
|
||||||
"user_id_unique": {
|
|
||||||
"name": "user_id_unique",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
},
|
|
||||||
"user_username_unique": {
|
|
||||||
"name": "user_username_unique",
|
|
||||||
"columns": [
|
|
||||||
"username"
|
|
||||||
],
|
|
||||||
"isUnique": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
},
|
||||||
"users_to_sandboxes": {
|
"compositePrimaryKeys": {},
|
||||||
"name": "users_to_sandboxes",
|
"uniqueConstraints": {}
|
||||||
"columns": {
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sandboxId": {
|
|
||||||
"name": "sandboxId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sharedOn": {
|
|
||||||
"name": "sharedOn",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"users_to_sandboxes_userId_user_id_fk": {
|
|
||||||
"name": "users_to_sandboxes_userId_user_id_fk",
|
|
||||||
"tableFrom": "users_to_sandboxes",
|
|
||||||
"tableTo": "user",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"users_to_sandboxes_sandboxId_sandbox_id_fk": {
|
|
||||||
"name": "users_to_sandboxes_sandboxId_sandbox_id_fk",
|
|
||||||
"tableFrom": "users_to_sandboxes",
|
|
||||||
"tableTo": "sandbox",
|
|
||||||
"columnsFrom": [
|
|
||||||
"sandboxId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"enums": {},
|
"user": {
|
||||||
"_meta": {
|
"name": "user",
|
||||||
"schemas": {},
|
"columns": {
|
||||||
"tables": {},
|
"id": {
|
||||||
"columns": {}
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"avatarUrl": {
|
||||||
|
"name": "avatarUrl",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "CURRENT_TIMESTAMP"
|
||||||
|
},
|
||||||
|
"generations": {
|
||||||
|
"name": "generations",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"tier": {
|
||||||
|
"name": "tier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'FREE'"
|
||||||
|
},
|
||||||
|
"tierExpiresAt": {
|
||||||
|
"name": "tierExpiresAt",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lastResetDate": {
|
||||||
|
"name": "lastResetDate",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_id_unique": {
|
||||||
|
"name": "user_id_unique",
|
||||||
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"user_username_unique": {
|
||||||
|
"name": "user_username_unique",
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
|
},
|
||||||
|
"users_to_sandboxes": {
|
||||||
|
"name": "users_to_sandboxes",
|
||||||
|
"columns": {
|
||||||
|
"userId": {
|
||||||
|
"name": "userId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sandboxId": {
|
||||||
|
"name": "sandboxId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"sharedOn": {
|
||||||
|
"name": "sharedOn",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"users_to_sandboxes_userId_user_id_fk": {
|
||||||
|
"name": "users_to_sandboxes_userId_user_id_fk",
|
||||||
|
"tableFrom": "users_to_sandboxes",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"users_to_sandboxes_sandboxId_sandbox_id_fk": {
|
||||||
|
"name": "users_to_sandboxes_sandboxId_sandbox_id_fk",
|
||||||
|
"tableFrom": "users_to_sandboxes",
|
||||||
|
"tableTo": "sandbox",
|
||||||
|
"columnsFrom": [
|
||||||
|
"sandboxId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "no action",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
}
|
}
|
||||||
|
}
|
@ -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": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,22 +5,15 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1736155854410,
|
"when": 1732400315508,
|
||||||
"tag": "0000_sudden_wallop",
|
"tag": "0000_milky_hardball",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1736169498666,
|
"when": 1732408530094,
|
||||||
"tag": "0001_dusty_komodo",
|
"tag": "0001_freezing_supreme_intelligence",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1736768910615,
|
|
||||||
"tag": "0002_chemical_brother_voodoo",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
1960
backend/database/package-lock.json
generated
1960
backend/database/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,7 @@
|
|||||||
"drizzle-kit": "^0.20.17",
|
"drizzle-kit": "^0.20.17",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"vitest": "1.3.0",
|
"vitest": "1.3.0",
|
||||||
"wrangler": "^3.101.0"
|
"wrangler": "^3.86.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
|
@ -5,599 +5,379 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { and, eq, sql } from "drizzle-orm"
|
import { and, eq, sql } from "drizzle-orm"
|
||||||
import * as schema from "./schema"
|
import * as schema from "./schema"
|
||||||
import { Sandbox, sandbox, sandboxLikes, user, usersToSandboxes } from "./schema"
|
import { sandbox, user, usersToSandboxes } from "./schema"
|
||||||
|
|
||||||
export interface Env {
|
export interface Env {
|
||||||
DB: D1Database
|
DB: D1Database
|
||||||
STORAGE: any
|
STORAGE: any
|
||||||
KEY: string
|
KEY: string
|
||||||
STORAGE_WORKER_URL: string
|
STORAGE_WORKER_URL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1
|
// https://github.com/drizzle-team/drizzle-orm/tree/main/examples/cloudflare-d1
|
||||||
|
|
||||||
// npm run generate
|
// npm run generate
|
||||||
// npx wrangler d1 execute d1-sandbox --local --file=./drizzle/<FILE>
|
// npx wrangler d1 execute d1-sandbox --local --file=./drizzle/<FILE>
|
||||||
interface SandboxWithLiked extends Sandbox {
|
|
||||||
liked: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserResponse extends Omit<schema.User, "sandbox"> {
|
|
||||||
sandbox: SandboxWithLiked[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(
|
async fetch(
|
||||||
request: Request,
|
request: Request,
|
||||||
env: Env,
|
env: Env,
|
||||||
ctx: ExecutionContext
|
ctx: ExecutionContext
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const success = new Response("Success", { status: 200 })
|
const success = new Response("Success", { status: 200 })
|
||||||
const invalidRequest = new Response("Invalid Request", { status: 400 })
|
const invalidRequest = new Response("Invalid Request", { status: 400 })
|
||||||
const notFound = new Response("Not Found", { status: 404 })
|
const notFound = new Response("Not Found", { status: 404 })
|
||||||
const methodNotAllowed = new Response("Method Not Allowed", { status: 405 })
|
const methodNotAllowed = new Response("Method Not Allowed", { status: 405 })
|
||||||
|
|
||||||
const url = new URL(request.url)
|
const url = new URL(request.url)
|
||||||
const path = url.pathname
|
const path = url.pathname
|
||||||
const method = request.method
|
const method = request.method
|
||||||
|
|
||||||
if (request.headers.get("Authorization") !== env.KEY) {
|
if (request.headers.get("Authorization") !== env.KEY) {
|
||||||
return new Response("Unauthorized", { status: 401 })
|
return new Response("Unauthorized", { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = drizzle(env.DB, { schema })
|
const db = drizzle(env.DB, { schema })
|
||||||
|
|
||||||
if (path === "/api/sandbox") {
|
if (path === "/api/sandbox") {
|
||||||
if (method === "GET") {
|
if (method === "GET") {
|
||||||
const params = url.searchParams
|
const params = url.searchParams
|
||||||
if (params.has("id")) {
|
if (params.has("id")) {
|
||||||
const id = params.get("id") as string
|
const id = params.get("id") as string
|
||||||
const res = await db.query.sandbox.findFirst({
|
const res = await db.query.sandbox.findFirst({
|
||||||
where: (sandbox, { eq }) => eq(sandbox.id, id),
|
where: (sandbox, { eq }) => eq(sandbox.id, id),
|
||||||
with: {
|
with: {
|
||||||
usersToSandboxes: true,
|
usersToSandboxes: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return json(res ?? {})
|
return json(res ?? {})
|
||||||
} else {
|
} else {
|
||||||
const res = await db.select().from(sandbox).all()
|
const res = await db.select().from(sandbox).all()
|
||||||
return json(res ?? {})
|
return json(res ?? {})
|
||||||
}
|
}
|
||||||
} else if (method === "DELETE") {
|
} else if (method === "DELETE") {
|
||||||
const params = url.searchParams
|
const params = url.searchParams
|
||||||
if (params.has("id")) {
|
if (params.has("id")) {
|
||||||
const id = params.get("id") as string
|
const id = params.get("id") as string
|
||||||
await db.delete(sandboxLikes).where(eq(sandboxLikes.sandboxId, id))
|
await db
|
||||||
await db
|
.delete(usersToSandboxes)
|
||||||
.delete(usersToSandboxes)
|
.where(eq(usersToSandboxes.sandboxId, id))
|
||||||
.where(eq(usersToSandboxes.sandboxId, id))
|
await db.delete(sandbox).where(eq(sandbox.id, id))
|
||||||
await db.delete(sandbox).where(eq(sandbox.id, id))
|
|
||||||
|
|
||||||
const deleteStorageRequest = new Request(
|
const deleteStorageRequest = new Request(
|
||||||
`${env.STORAGE_WORKER_URL}/api/project`,
|
`${env.STORAGE_WORKER_URL}/api/project`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify({ sandboxId: id }),
|
body: JSON.stringify({ sandboxId: id }),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `${env.KEY}`,
|
Authorization: `${env.KEY}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await env.STORAGE.fetch(deleteStorageRequest)
|
await env.STORAGE.fetch(deleteStorageRequest)
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} else {
|
} else {
|
||||||
return invalidRequest
|
return invalidRequest
|
||||||
}
|
}
|
||||||
} else if (method === "POST") {
|
} else if (method === "POST") {
|
||||||
const postSchema = z.object({
|
const postSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
visibility: z.enum(["public", "private"]).optional(),
|
visibility: z.enum(["public", "private"]).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { id, name, visibility } = postSchema.parse(body)
|
const { id, name, visibility } = postSchema.parse(body)
|
||||||
const sb = await db
|
const sb = await db
|
||||||
.update(sandbox)
|
.update(sandbox)
|
||||||
.set({ name, visibility })
|
.set({ name, visibility })
|
||||||
.where(eq(sandbox.id, id))
|
.where(eq(sandbox.id, id))
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} else if (method === "PUT") {
|
} else if (method === "PUT") {
|
||||||
const initSchema = z.object({
|
const initSchema = z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
visibility: z.enum(["public", "private"]),
|
visibility: z.enum(["public", "private"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { type, name, userId, visibility } = initSchema.parse(body)
|
const { type, name, userId, visibility } = initSchema.parse(body)
|
||||||
|
|
||||||
const userSandboxes = await db
|
const userSandboxes = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sandbox)
|
.from(sandbox)
|
||||||
.where(eq(sandbox.userId, userId))
|
.where(eq(sandbox.userId, userId))
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
if (userSandboxes.length >= 8) {
|
if (userSandboxes.length >= 8) {
|
||||||
return new Response("You reached the maximum # of sandboxes.", {
|
return new Response("You reached the maximum # of sandboxes.", {
|
||||||
status: 400,
|
status: 400,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sb = await db
|
const sb = await db
|
||||||
.insert(sandbox)
|
.insert(sandbox)
|
||||||
.values({ type, name, userId, visibility, createdAt: new Date() })
|
.values({ type, name, userId, visibility, createdAt: new Date() })
|
||||||
.returning()
|
.returning()
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
const initStorageRequest = new Request(
|
const initStorageRequest = new Request(
|
||||||
`${env.STORAGE_WORKER_URL}/api/init`,
|
`${env.STORAGE_WORKER_URL}/api/init`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ sandboxId: sb.id, type }),
|
body: JSON.stringify({ sandboxId: sb.id, type }),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `${env.KEY}`,
|
Authorization: `${env.KEY}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await env.STORAGE.fetch(initStorageRequest)
|
await env.STORAGE.fetch(initStorageRequest)
|
||||||
|
|
||||||
return new Response(sb.id, { status: 200 })
|
return new Response(sb.id, { status: 200 })
|
||||||
} else {
|
} else {
|
||||||
return methodNotAllowed
|
return methodNotAllowed
|
||||||
}
|
}
|
||||||
} else if (path === "/api/sandbox/share") {
|
} else if (path === "/api/sandbox/share") {
|
||||||
if (method === "GET") {
|
if (method === "GET") {
|
||||||
const params = url.searchParams
|
const params = url.searchParams
|
||||||
if (params.has("id")) {
|
if (params.has("id")) {
|
||||||
const id = params.get("id") as string
|
const id = params.get("id") as string
|
||||||
const res = await db.query.usersToSandboxes.findMany({
|
const res = await db.query.usersToSandboxes.findMany({
|
||||||
where: (uts, { eq }) => eq(uts.userId, id),
|
where: (uts, { eq }) => eq(uts.userId, id),
|
||||||
})
|
})
|
||||||
|
|
||||||
const owners = await Promise.all(
|
const owners = await Promise.all(
|
||||||
res.map(async (r) => {
|
res.map(async (r) => {
|
||||||
const sb = await db.query.sandbox.findFirst({
|
const sb = await db.query.sandbox.findFirst({
|
||||||
where: (sandbox, { eq }) => eq(sandbox.id, r.sandboxId),
|
where: (sandbox, { eq }) => eq(sandbox.id, r.sandboxId),
|
||||||
with: {
|
with: {
|
||||||
author: true,
|
author: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (!sb) return
|
if (!sb) return
|
||||||
return {
|
return {
|
||||||
id: sb.id,
|
id: sb.id,
|
||||||
name: sb.name,
|
name: sb.name,
|
||||||
type: sb.type,
|
type: sb.type,
|
||||||
author: sb.author.name,
|
author: sb.author.name,
|
||||||
authorAvatarUrl: sb.author.avatarUrl,
|
authorAvatarUrl: sb.author.avatarUrl,
|
||||||
sharedOn: r.sharedOn,
|
sharedOn: r.sharedOn,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
return json(owners ?? {})
|
return json(owners ?? {})
|
||||||
} else return invalidRequest
|
} else return invalidRequest
|
||||||
} else if (method === "POST") {
|
} else if (method === "POST") {
|
||||||
const shareSchema = z.object({
|
const shareSchema = z.object({
|
||||||
sandboxId: z.string(),
|
sandboxId: z.string(),
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { sandboxId, email } = shareSchema.parse(body)
|
const { sandboxId, email } = shareSchema.parse(body)
|
||||||
|
|
||||||
const user = await db.query.user.findFirst({
|
const user = await db.query.user.findFirst({
|
||||||
where: (user, { eq }) => eq(user.email, email),
|
where: (user, { eq }) => eq(user.email, email),
|
||||||
with: {
|
with: {
|
||||||
sandbox: true,
|
sandbox: true,
|
||||||
usersToSandboxes: true,
|
usersToSandboxes: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return new Response("No user associated with email.", { status: 400 })
|
return new Response("No user associated with email.", { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.sandbox.find((sb) => sb.id === sandboxId)) {
|
if (user.sandbox.find((sb) => sb.id === sandboxId)) {
|
||||||
return new Response("Cannot share with yourself!", { status: 400 })
|
return new Response("Cannot share with yourself!", { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.usersToSandboxes.find((uts) => uts.sandboxId === sandboxId)) {
|
if (user.usersToSandboxes.find((uts) => uts.sandboxId === sandboxId)) {
|
||||||
return new Response("User already has access.", { status: 400 })
|
return new Response("User already has access.", { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.insert(usersToSandboxes)
|
.insert(usersToSandboxes)
|
||||||
.values({ userId: user.id, sandboxId, sharedOn: new Date() })
|
.values({ userId: user.id, sandboxId, sharedOn: new Date() })
|
||||||
.get()
|
.get()
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} else if (method === "DELETE") {
|
} else if (method === "DELETE") {
|
||||||
const deleteShareSchema = z.object({
|
const deleteShareSchema = z.object({
|
||||||
sandboxId: z.string(),
|
sandboxId: z.string(),
|
||||||
userId: z.string(),
|
userId: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
const { sandboxId, userId } = deleteShareSchema.parse(body)
|
const { sandboxId, userId } = deleteShareSchema.parse(body)
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.delete(usersToSandboxes)
|
.delete(usersToSandboxes)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(usersToSandboxes.userId, userId),
|
eq(usersToSandboxes.userId, userId),
|
||||||
eq(usersToSandboxes.sandboxId, sandboxId)
|
eq(usersToSandboxes.sandboxId, sandboxId)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return success
|
return success
|
||||||
} else return methodNotAllowed
|
} else return methodNotAllowed
|
||||||
} else if (path === "/api/sandbox/like") {
|
} else if (path === "/api/user") {
|
||||||
if (method === "POST") {
|
if (method === "GET") {
|
||||||
const likeSchema = z.object({
|
const params = url.searchParams
|
||||||
sandboxId: z.string(),
|
|
||||||
userId: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
if (params.has("id")) {
|
||||||
const body = await request.json()
|
const id = params.get("id") as string
|
||||||
const { sandboxId, userId } = likeSchema.parse(body)
|
const res = await db.query.user.findFirst({
|
||||||
|
where: (user, { eq }) => eq(user.id, id),
|
||||||
|
with: {
|
||||||
|
sandbox: {
|
||||||
|
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
||||||
|
},
|
||||||
|
usersToSandboxes: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return json(res ?? {})
|
||||||
|
} else {
|
||||||
|
const res = await db.select().from(user).all()
|
||||||
|
return json(res ?? {})
|
||||||
|
}
|
||||||
|
} else if (method === "POST") {
|
||||||
|
const userSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string().email(),
|
||||||
|
username: z.string(),
|
||||||
|
avatarUrl: z.string().optional(),
|
||||||
|
createdAt: z.string().optional(),
|
||||||
|
generations: z.number().optional(),
|
||||||
|
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]).optional(),
|
||||||
|
tierExpiresAt: z.number().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
// Check if user has already liked
|
const body = await request.json()
|
||||||
const existingLike = await db.query.sandboxLikes.findFirst({
|
const { id, name, email, username, avatarUrl, createdAt, generations, tier, tierExpiresAt } = userSchema.parse(body)
|
||||||
where: (likes, { and, eq }) =>
|
|
||||||
and(eq(likes.sandboxId, sandboxId), eq(likes.userId, userId)),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (existingLike) {
|
const res = await db
|
||||||
// Unlike
|
.insert(user)
|
||||||
await db
|
.values({
|
||||||
.delete(sandboxLikes)
|
id,
|
||||||
.where(
|
name,
|
||||||
and(
|
email,
|
||||||
eq(sandboxLikes.sandboxId, sandboxId),
|
username,
|
||||||
eq(sandboxLikes.userId, userId)
|
avatarUrl,
|
||||||
)
|
createdAt: createdAt ? new Date(createdAt) : new Date(),
|
||||||
)
|
generations,
|
||||||
|
tier,
|
||||||
|
tierExpiresAt,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
.get()
|
||||||
|
return json({ res })
|
||||||
|
} else if (method === "DELETE") {
|
||||||
|
const params = url.searchParams
|
||||||
|
if (params.has("id")) {
|
||||||
|
const id = params.get("id") as string
|
||||||
|
await db.delete(user).where(eq(user.id, id))
|
||||||
|
return success
|
||||||
|
} else return invalidRequest
|
||||||
|
} else {
|
||||||
|
return methodNotAllowed
|
||||||
|
}
|
||||||
|
} else if (path === "/api/user/check-username") {
|
||||||
|
if (method === "GET") {
|
||||||
|
const params = url.searchParams
|
||||||
|
const username = params.get("username")
|
||||||
|
|
||||||
await db
|
if (!username) return invalidRequest
|
||||||
.update(sandbox)
|
|
||||||
.set({
|
|
||||||
likeCount: sql`${sandbox.likeCount} - 1`,
|
|
||||||
})
|
|
||||||
.where(eq(sandbox.id, sandboxId))
|
|
||||||
|
|
||||||
return json({
|
const exists = await db.query.user.findFirst({
|
||||||
message: "Unlike successful",
|
where: (user, { eq }) => eq(user.username, username)
|
||||||
liked: false,
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Like
|
|
||||||
await db.insert(sandboxLikes).values({
|
|
||||||
sandboxId,
|
|
||||||
userId,
|
|
||||||
createdAt: new Date(),
|
|
||||||
})
|
|
||||||
|
|
||||||
await db
|
return json({ exists: !!exists })
|
||||||
.update(sandbox)
|
}
|
||||||
.set({
|
return methodNotAllowed
|
||||||
likeCount: sql`${sandbox.likeCount} + 1`,
|
} else if (path === "/api/user/increment-generations" && method === "POST") {
|
||||||
})
|
const schema = z.object({
|
||||||
.where(eq(sandbox.id, sandboxId))
|
userId: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
return json({
|
const body = await request.json()
|
||||||
message: "Like successful",
|
const { userId } = schema.parse(body)
|
||||||
liked: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return new Response("Invalid request format", { status: 400 })
|
|
||||||
}
|
|
||||||
} else if (method === "GET") {
|
|
||||||
const params = url.searchParams
|
|
||||||
const sandboxId = params.get("sandboxId")
|
|
||||||
const userId = params.get("userId")
|
|
||||||
|
|
||||||
if (!sandboxId || !userId) {
|
await db
|
||||||
return invalidRequest
|
.update(user)
|
||||||
}
|
.set({ generations: sql`${user.generations} + 1` })
|
||||||
|
.where(eq(user.id, userId))
|
||||||
|
.get()
|
||||||
|
|
||||||
const like = await db.query.sandboxLikes.findFirst({
|
return success
|
||||||
where: (likes, { and, eq }) =>
|
} else if (path === "/api/user/update-tier" && method === "POST") {
|
||||||
and(eq(likes.sandboxId, sandboxId), eq(likes.userId, userId)),
|
const schema = z.object({
|
||||||
})
|
userId: z.string(),
|
||||||
|
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]),
|
||||||
|
tierExpiresAt: z.date(),
|
||||||
|
})
|
||||||
|
|
||||||
return json({
|
const body = await request.json()
|
||||||
liked: !!like,
|
const { userId, tier, tierExpiresAt } = schema.parse(body)
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return methodNotAllowed
|
|
||||||
}
|
|
||||||
} else if (path === "/api/user") {
|
|
||||||
if (method === "GET") {
|
|
||||||
const params = url.searchParams
|
|
||||||
|
|
||||||
if (params.has("id")) {
|
await db
|
||||||
const id = params.get("id") as string
|
.update(user)
|
||||||
|
.set({
|
||||||
|
tier,
|
||||||
|
tierExpiresAt: tierExpiresAt.getTime(),
|
||||||
|
// Reset generations when upgrading tier
|
||||||
|
generations: 0
|
||||||
|
})
|
||||||
|
.where(eq(user.id, userId))
|
||||||
|
.get()
|
||||||
|
|
||||||
const res = await db.query.user.findFirst({
|
return success
|
||||||
where: (user, { eq }) => eq(user.id, id),
|
} else if (path === "/api/user/check-reset" && method === "POST") {
|
||||||
with: {
|
const schema = z.object({
|
||||||
sandbox: {
|
userId: z.string(),
|
||||||
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
})
|
||||||
with: {
|
|
||||||
likes: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
usersToSandboxes: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (res) {
|
|
||||||
const transformedUser: UserResponse = {
|
|
||||||
...res,
|
|
||||||
sandbox: res.sandbox.map(
|
|
||||||
(sb): SandboxWithLiked => ({
|
|
||||||
...sb,
|
|
||||||
liked: sb.likes.some((like) => like.userId === id),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}
|
|
||||||
return json(transformedUser)
|
|
||||||
}
|
|
||||||
return json(res ?? {})
|
|
||||||
} else if (params.has("username")) {
|
|
||||||
const username = params.get("username") as string
|
|
||||||
const userId = params.get("currentUserId")
|
|
||||||
const res = await db.query.user.findFirst({
|
|
||||||
where: (user, { eq }) => eq(user.username, username),
|
|
||||||
with: {
|
|
||||||
sandbox: {
|
|
||||||
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
|
||||||
with: {
|
|
||||||
likes: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
usersToSandboxes: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (res) {
|
|
||||||
const transformedUser: UserResponse = {
|
|
||||||
...res,
|
|
||||||
sandbox: res.sandbox.map(
|
|
||||||
(sb): SandboxWithLiked => ({
|
|
||||||
...sb,
|
|
||||||
liked: sb.likes.some((like) => like.userId === userId),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}
|
|
||||||
return json(transformedUser)
|
|
||||||
}
|
|
||||||
return json(res ?? {})
|
|
||||||
} else {
|
|
||||||
const res = await db.select().from(user).all()
|
|
||||||
return json(res ?? {})
|
|
||||||
}
|
|
||||||
} else if (method === "POST") {
|
|
||||||
const userSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
email: z.string().email(),
|
|
||||||
username: z.string(),
|
|
||||||
avatarUrl: z.string().optional(),
|
|
||||||
githubToken: z.string().nullable().optional(),
|
|
||||||
createdAt: z.string().optional(),
|
|
||||||
generations: z.number().optional(),
|
|
||||||
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]).optional(),
|
|
||||||
tierExpiresAt: z.number().optional(),
|
|
||||||
lastResetDate: z.number().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const body = await request.json()
|
const body = await request.json()
|
||||||
|
const { userId } = schema.parse(body)
|
||||||
|
|
||||||
const {
|
const dbUser = await db.query.user.findFirst({
|
||||||
id,
|
where: (user, { eq }) => eq(user.id, userId),
|
||||||
name,
|
})
|
||||||
email,
|
|
||||||
username,
|
|
||||||
avatarUrl,
|
|
||||||
githubToken,
|
|
||||||
createdAt,
|
|
||||||
generations,
|
|
||||||
tier,
|
|
||||||
tierExpiresAt,
|
|
||||||
lastResetDate,
|
|
||||||
} = userSchema.parse(body)
|
|
||||||
const res = await db
|
|
||||||
.insert(user)
|
|
||||||
.values({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
username,
|
|
||||||
avatarUrl,
|
|
||||||
githubToken,
|
|
||||||
createdAt: createdAt ? new Date(createdAt) : new Date(),
|
|
||||||
generations,
|
|
||||||
tier,
|
|
||||||
tierExpiresAt,
|
|
||||||
lastResetDate,
|
|
||||||
})
|
|
||||||
.returning()
|
|
||||||
.get()
|
|
||||||
return json({ res })
|
|
||||||
} else if (method === "DELETE") {
|
|
||||||
const params = url.searchParams
|
|
||||||
if (params.has("id")) {
|
|
||||||
const id = params.get("id") as string
|
|
||||||
await db.delete(user).where(eq(user.id, id))
|
|
||||||
return success
|
|
||||||
} else return invalidRequest
|
|
||||||
} else if (method === "PUT") {
|
|
||||||
const updateUserSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
bio: z.string().optional(),
|
|
||||||
personalWebsite: z.string().optional(),
|
|
||||||
links: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
url: z.string(),
|
|
||||||
platform: z.enum(schema.KNOWN_PLATFORMS),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
email: z.string().email().optional(),
|
|
||||||
username: z.string().optional(),
|
|
||||||
avatarUrl: z.string().optional(),
|
|
||||||
githubToken: z.string().nullable().optional(),
|
|
||||||
generations: z.number().optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
if (!dbUser) {
|
||||||
const body = await request.json()
|
return new Response("User not found", { status: 404 })
|
||||||
const validatedData = updateUserSchema.parse(body)
|
}
|
||||||
|
|
||||||
const { id, username, ...updateData } = validatedData
|
const now = new Date()
|
||||||
|
const lastReset = dbUser.lastResetDate ? new Date(dbUser.lastResetDate) : new Date(0)
|
||||||
|
|
||||||
// If username is being updated, check for existing username
|
if (now.getMonth() !== lastReset.getMonth() || now.getFullYear() !== lastReset.getFullYear()) {
|
||||||
if (username) {
|
await db
|
||||||
const existingUser = await db
|
.update(user)
|
||||||
.select()
|
.set({
|
||||||
.from(user)
|
generations: 0,
|
||||||
.where(eq(user.username, username))
|
lastResetDate: now.getTime()
|
||||||
.get()
|
})
|
||||||
if (existingUser && existingUser.id !== id) {
|
.where(eq(user.id, userId))
|
||||||
return json({ error: "Username already exists" }, { status: 409 })
|
.get()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanUpdateData = {
|
return new Response("Reset successful", { status: 200 })
|
||||||
...updateData,
|
}
|
||||||
...(username ? { username } : {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await db
|
return new Response("No reset needed", { status: 200 })
|
||||||
.update(user)
|
} else return notFound
|
||||||
.set(cleanUpdateData)
|
},
|
||||||
.where(eq(user.id, id))
|
|
||||||
.returning()
|
|
||||||
.get()
|
|
||||||
|
|
||||||
if (!res) {
|
|
||||||
return json({ error: "User not found" }, { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
return json({ res })
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return json({ error: error.errors }, { status: 400 })
|
|
||||||
}
|
|
||||||
return json({ error: "Internal server error" }, { status: 500 })
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return methodNotAllowed
|
|
||||||
}
|
|
||||||
} else if (path === "/api/user/check-username") {
|
|
||||||
if (method === "GET") {
|
|
||||||
const params = url.searchParams
|
|
||||||
const username = params.get("username")
|
|
||||||
|
|
||||||
if (!username) return invalidRequest
|
|
||||||
|
|
||||||
const exists = await db.query.user.findFirst({
|
|
||||||
where: (user, { eq }) => eq(user.username, username),
|
|
||||||
})
|
|
||||||
|
|
||||||
return json({ exists: !!exists })
|
|
||||||
}
|
|
||||||
return methodNotAllowed
|
|
||||||
} else if (
|
|
||||||
path === "/api/user/increment-generations" &&
|
|
||||||
method === "POST"
|
|
||||||
) {
|
|
||||||
const schema = z.object({
|
|
||||||
userId: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { userId } = schema.parse(body)
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(user)
|
|
||||||
.set({ generations: sql`${user.generations} + 1` })
|
|
||||||
.where(eq(user.id, userId))
|
|
||||||
.get()
|
|
||||||
|
|
||||||
return success
|
|
||||||
} else if (path === "/api/user/update-tier" && method === "POST") {
|
|
||||||
const schema = z.object({
|
|
||||||
userId: z.string(),
|
|
||||||
tier: z.enum(["FREE", "PRO", "ENTERPRISE"]),
|
|
||||||
tierExpiresAt: z.date(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { userId, tier, tierExpiresAt } = schema.parse(body)
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(user)
|
|
||||||
.set({
|
|
||||||
tier,
|
|
||||||
tierExpiresAt: tierExpiresAt.getTime(),
|
|
||||||
// Reset generations when upgrading tier
|
|
||||||
generations: 0,
|
|
||||||
})
|
|
||||||
.where(eq(user.id, userId))
|
|
||||||
.get()
|
|
||||||
|
|
||||||
return success
|
|
||||||
} else if (path === "/api/user/check-reset" && method === "POST") {
|
|
||||||
const schema = z.object({
|
|
||||||
userId: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { userId } = schema.parse(body)
|
|
||||||
|
|
||||||
const dbUser = await db.query.user.findFirst({
|
|
||||||
where: (user, { eq }) => eq(user.id, userId),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!dbUser) {
|
|
||||||
return new Response("User not found", { status: 404 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
const lastReset = dbUser.lastResetDate
|
|
||||||
? new Date(dbUser.lastResetDate)
|
|
||||||
: new Date(0)
|
|
||||||
|
|
||||||
if (
|
|
||||||
now.getMonth() !== lastReset.getMonth() ||
|
|
||||||
now.getFullYear() !== lastReset.getFullYear()
|
|
||||||
) {
|
|
||||||
await db
|
|
||||||
.update(user)
|
|
||||||
.set({
|
|
||||||
generations: 0,
|
|
||||||
lastResetDate: now.getTime(),
|
|
||||||
})
|
|
||||||
.where(eq(user.id, userId))
|
|
||||||
.get()
|
|
||||||
|
|
||||||
return new Response("Reset successful", { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response("No reset needed", { status: 200 })
|
|
||||||
} else return notFound
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
@ -1,140 +1,79 @@
|
|||||||
import { createId } from "@paralleldrive/cuid2"
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { relations, sql } from "drizzle-orm"
|
import { relations } from "drizzle-orm"
|
||||||
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||||
|
import { sql } from "drizzle-orm"
|
||||||
|
|
||||||
export const KNOWN_PLATFORMS = [
|
|
||||||
"github",
|
|
||||||
"twitter",
|
|
||||||
"instagram",
|
|
||||||
"bluesky",
|
|
||||||
"linkedin",
|
|
||||||
"youtube",
|
|
||||||
"twitch",
|
|
||||||
"discord",
|
|
||||||
"mastodon",
|
|
||||||
"threads",
|
|
||||||
"gitlab",
|
|
||||||
"generic",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type KnownPlatform = (typeof KNOWN_PLATFORMS)[number]
|
|
||||||
export type UserLink = {
|
|
||||||
url: string
|
|
||||||
platform: KnownPlatform
|
|
||||||
}
|
|
||||||
// #region Tables
|
|
||||||
export const user = sqliteTable("user", {
|
export const user = sqliteTable("user", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
.$defaultFn(() => createId())
|
.$defaultFn(() => createId())
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.unique(),
|
.unique(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
username: text("username").notNull().unique(),
|
username: text("username").notNull().unique(),
|
||||||
avatarUrl: text("avatarUrl"),
|
avatarUrl: text("avatarUrl"),
|
||||||
githubToken: text("githubToken"),
|
createdAt: integer("createdAt", { mode: "timestamp_ms" })
|
||||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
sql`CURRENT_TIMESTAMP`
|
generations: integer("generations").default(0),
|
||||||
),
|
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
|
||||||
generations: integer("generations").default(0),
|
tierExpiresAt: integer("tierExpiresAt"),
|
||||||
bio: text("bio"),
|
lastResetDate: integer("lastResetDate"),
|
||||||
personalWebsite: text("personalWebsite"),
|
|
||||||
links: text("links", { mode: "json" }).default("[]").$type<UserLink[]>(),
|
|
||||||
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
|
|
||||||
tierExpiresAt: integer("tierExpiresAt"),
|
|
||||||
lastResetDate: integer("lastResetDate"),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type User = typeof user.$inferSelect
|
export type User = typeof user.$inferSelect
|
||||||
|
|
||||||
|
export const userRelations = relations(user, ({ many }) => ({
|
||||||
|
sandbox: many(sandbox),
|
||||||
|
usersToSandboxes: many(usersToSandboxes),
|
||||||
|
}))
|
||||||
|
|
||||||
export const sandbox = sqliteTable("sandbox", {
|
export const sandbox = sqliteTable("sandbox", {
|
||||||
id: text("id")
|
id: text("id")
|
||||||
.$defaultFn(() => createId())
|
.$defaultFn(() => createId())
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.unique(),
|
.unique(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
type: text("type").notNull(),
|
type: text("type").notNull(),
|
||||||
visibility: text("visibility", { enum: ["public", "private"] }),
|
visibility: text("visibility", { enum: ["public", "private"] }),
|
||||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
createdAt: integer("createdAt", { mode: "timestamp_ms" })
|
||||||
sql`CURRENT_TIMESTAMP`
|
.default(sql`CURRENT_TIMESTAMP`),
|
||||||
),
|
userId: text("user_id")
|
||||||
userId: text("user_id")
|
.notNull()
|
||||||
.notNull()
|
.references(() => user.id),
|
||||||
.references(() => user.id),
|
likeCount: integer("likeCount").default(0),
|
||||||
likeCount: integer("likeCount").default(0),
|
viewCount: integer("viewCount").default(0),
|
||||||
viewCount: integer("viewCount").default(0),
|
|
||||||
containerId: text("containerId"),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Sandbox = typeof sandbox.$inferSelect
|
export type Sandbox = typeof sandbox.$inferSelect
|
||||||
|
|
||||||
export const sandboxLikes = sqliteTable(
|
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
||||||
"sandbox_likes",
|
author: one(user, {
|
||||||
{
|
fields: [sandbox.userId],
|
||||||
userId: text("user_id")
|
references: [user.id],
|
||||||
.notNull()
|
}),
|
||||||
.references(() => user.id),
|
usersToSandboxes: many(usersToSandboxes),
|
||||||
sandboxId: text("sandbox_id")
|
}))
|
||||||
.notNull()
|
|
||||||
.references(() => sandbox.id),
|
|
||||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
|
||||||
sql`CURRENT_TIMESTAMP`
|
|
||||||
),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
pk: primaryKey({ columns: [table.sandboxId, table.userId] }),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
||||||
userId: text("userId")
|
userId: text("userId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => user.id),
|
.references(() => user.id),
|
||||||
sandboxId: text("sandboxId")
|
sandboxId: text("sandboxId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sandbox.id),
|
.references(() => sandbox.id),
|
||||||
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
||||||
})
|
})
|
||||||
|
|
||||||
// #region Relations
|
|
||||||
export const userRelations = relations(user, ({ many }) => ({
|
|
||||||
sandbox: many(sandbox),
|
|
||||||
usersToSandboxes: many(usersToSandboxes),
|
|
||||||
likes: many(sandboxLikes),
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
|
||||||
author: one(user, {
|
|
||||||
fields: [sandbox.userId],
|
|
||||||
references: [user.id],
|
|
||||||
}),
|
|
||||||
usersToSandboxes: many(usersToSandboxes),
|
|
||||||
likes: many(sandboxLikes),
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const sandboxLikesRelations = relations(sandboxLikes, ({ one }) => ({
|
|
||||||
user: one(user, {
|
|
||||||
fields: [sandboxLikes.userId],
|
|
||||||
references: [user.id],
|
|
||||||
}),
|
|
||||||
sandbox: one(sandbox, {
|
|
||||||
fields: [sandboxLikes.sandboxId],
|
|
||||||
references: [sandbox.id],
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const usersToSandboxesRelations = relations(
|
export const usersToSandboxesRelations = relations(
|
||||||
usersToSandboxes,
|
usersToSandboxes,
|
||||||
({ one }) => ({
|
({ one }) => ({
|
||||||
group: one(sandbox, {
|
group: one(sandbox, {
|
||||||
fields: [usersToSandboxes.sandboxId],
|
fields: [usersToSandboxes.sandboxId],
|
||||||
references: [sandbox.id],
|
references: [sandbox.id],
|
||||||
}),
|
}),
|
||||||
user: one(user, {
|
user: one(user, {
|
||||||
fields: [usersToSandboxes.userId],
|
fields: [usersToSandboxes.userId],
|
||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
// #endregion
|
|
||||||
|
@ -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=
|
||||||
|
433
backend/server/package-lock.json
generated
433
backend/server/package-lock.json
generated
@ -33,9 +33,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.26.0",
|
"version": "7.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
|
||||||
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
|
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
},
|
},
|
||||||
@ -111,11 +111,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@kwsites/file-exists/node_modules/debug": {
|
"node_modules/@kwsites/file-exists/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@ -127,9 +127,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@kwsites/file-exists/node_modules/ms": {
|
"node_modules/@kwsites/file-exists/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/@kwsites/promise-deferred": {
|
"node_modules/@kwsites/promise-deferred": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@ -210,9 +210,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/express-serve-static-core": {
|
"node_modules/@types/express-serve-static-core": {
|
||||||
"version": "4.19.6",
|
"version": "4.19.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz",
|
||||||
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
"integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@ -244,17 +244,17 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.17.6",
|
"version": "20.14.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz",
|
||||||
"integrity": "sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==",
|
"integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.17",
|
"version": "6.9.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
||||||
"integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
|
"integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/range-parser": {
|
"node_modules/@types/range-parser": {
|
||||||
@ -294,20 +294,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/ssh2/node_modules/@types/node": {
|
"node_modules/@types/ssh2/node_modules/@types/node": {
|
||||||
"version": "18.19.64",
|
"version": "18.19.44",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.64.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.44.tgz",
|
||||||
"integrity": "sha512-955mDqvO2vFf/oL7V3WiUtiz+BugyX8uVbaT2H8oj3+8dRyH2FLiNdowe7eNqRM7IOIZvzDH76EoAT+gwm6aIQ==",
|
"integrity": "sha512-ZsbGerYg72WMXUIE9fYxtvfzLEuq6q8mKERdWFnqTmOvudMxnz+CBNRoOwJ2kNpFOncrKjT1hZwxjlFgQ9qvQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/ssh2/node_modules/undici-types": {
|
|
||||||
"version": "5.26.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@ -321,9 +315,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.0",
|
"version": "8.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
@ -333,9 +327,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn-walk": {
|
"node_modules/acorn-walk": {
|
||||||
"version": "8.3.4",
|
"version": "8.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
|
||||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.11.0"
|
"acorn": "^8.11.0"
|
||||||
@ -433,9 +427,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"content-type": "~1.0.5",
|
"content-type": "~1.0.5",
|
||||||
@ -445,7 +439,7 @@
|
|||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "0.4.24",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"qs": "6.13.0",
|
"qs": "6.11.0",
|
||||||
"raw-body": "2.5.2",
|
"raw-body": "2.5.2",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "1.0.0"
|
||||||
@ -477,6 +471,20 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bufferutil": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buildcheck": {
|
"node_modules/buildcheck": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz",
|
||||||
@ -648,9 +656,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cookie": {
|
"node_modules/cookie": {
|
||||||
"version": "0.7.1",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -801,24 +809,24 @@
|
|||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
},
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io": {
|
"node_modules/engine.io": {
|
||||||
"version": "6.6.2",
|
"version": "6.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz",
|
||||||
"integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
|
"integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/node": ">=10.0.0",
|
"@types/node": ">=10.0.0",
|
||||||
"accepts": "~1.3.4",
|
"accepts": "~1.3.4",
|
||||||
"base64id": "2.0.0",
|
"base64id": "2.0.0",
|
||||||
"cookie": "~0.7.2",
|
"cookie": "~0.4.1",
|
||||||
"cors": "~2.8.5",
|
"cors": "~2.8.5",
|
||||||
"debug": "~4.3.1",
|
"debug": "~4.3.1",
|
||||||
"engine.io-parser": "~5.2.1",
|
"engine.io-parser": "~5.2.1",
|
||||||
@ -837,19 +845,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io/node_modules/cookie": {
|
"node_modules/engine.io/node_modules/cookie": {
|
||||||
"version": "0.7.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io/node_modules/debug": {
|
"node_modules/engine.io/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@ -861,9 +869,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io/node_modules/ms": {
|
"node_modules/engine.io/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -885,9 +913,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@ -906,36 +934,36 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.1",
|
"version": "4.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "1.20.2",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.7.1",
|
"cookie": "0.6.0",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"encodeurl": "~2.0.0",
|
"encodeurl": "~1.0.2",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"finalhandler": "1.3.1",
|
"finalhandler": "1.2.0",
|
||||||
"fresh": "0.5.2",
|
"fresh": "0.5.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"merge-descriptors": "1.0.3",
|
"merge-descriptors": "1.0.1",
|
||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.10",
|
"path-to-regexp": "0.1.7",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.13.0",
|
"qs": "6.11.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "0.19.0",
|
"send": "0.18.0",
|
||||||
"serve-static": "1.16.2",
|
"serve-static": "1.15.0",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": "2.0.1",
|
"statuses": "2.0.1",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
@ -959,12 +987,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"encodeurl": "~2.0.0",
|
"encodeurl": "~1.0.2",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
@ -1267,12 +1295,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/merge-descriptors": {
|
"node_modules/merge-descriptors": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/methods": {
|
"node_modules/methods": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
@ -1330,9 +1355,9 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||||
},
|
},
|
||||||
"node_modules/nan": {
|
"node_modules/nan": {
|
||||||
"version": "2.22.0",
|
"version": "2.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz",
|
||||||
"integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==",
|
"integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
@ -1343,10 +1368,22 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz",
|
||||||
"integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
|
"integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.5.2",
|
"chokidar": "^3.5.2",
|
||||||
@ -1372,12 +1409,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemon/node_modules/debug": {
|
"node_modules/nodemon/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@ -1398,9 +1435,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemon/node_modules/ms": {
|
"node_modules/nodemon/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/nodemon/node_modules/supports-color": {
|
"node_modules/nodemon/node_modules/supports-color": {
|
||||||
@ -1433,9 +1470,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.3",
|
"version": "1.13.2",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||||
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
|
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
@ -1482,9 +1519,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.10",
|
"version": "0.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
|
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
@ -1528,11 +1565,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.0.6"
|
"side-channel": "^1.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
@ -1550,9 +1587,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rate-limiter-flexible": {
|
"node_modules/rate-limiter-flexible": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-5.0.3.tgz",
|
||||||
"integrity": "sha512-ftYHrIfSqWYDIJZ4yPTrgOduByAp+86gUS9iklv0JoXVM8eQCAjTnydCj1hAT4MmhmkSw86NaFEJ28m/LC1pKA=="
|
"integrity": "sha512-lWx2y8NBVlTOLPyqs+6y7dxfEpT6YFqKy3MzWbCy95sTTOhOuxufP2QvRyOHpfXpB9OUJPbVLybw3z3AVAS5fA=="
|
||||||
},
|
},
|
||||||
"node_modules/raw-body": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.2",
|
||||||
@ -1659,9 +1696,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.19.0",
|
"version": "0.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
@ -1681,28 +1718,20 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/send/node_modules/encodeurl": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/send/node_modules/ms": {
|
"node_modules/send/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "1.16.2",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"encodeurl": "~2.0.0",
|
"encodeurl": "~1.0.2",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"send": "0.19.0"
|
"send": "0.18.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
@ -1761,9 +1790,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-git": {
|
"node_modules/simple-git": {
|
||||||
"version": "3.27.0",
|
"version": "3.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.25.0.tgz",
|
||||||
"integrity": "sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==",
|
"integrity": "sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kwsites/file-exists": "^1.1.1",
|
"@kwsites/file-exists": "^1.1.1",
|
||||||
"@kwsites/promise-deferred": "^1.1.1",
|
"@kwsites/promise-deferred": "^1.1.1",
|
||||||
@ -1775,11 +1804,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-git/node_modules/debug": {
|
"node_modules/simple-git/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@ -1791,9 +1820,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-git/node_modules/ms": {
|
"node_modules/simple-git/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/simple-update-notifier": {
|
"node_modules/simple-update-notifier": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -1808,15 +1837,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io": {
|
"node_modules/socket.io": {
|
||||||
"version": "4.8.1",
|
"version": "4.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
|
||||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
"integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.4",
|
"accepts": "~1.3.4",
|
||||||
"base64id": "~2.0.0",
|
"base64id": "~2.0.0",
|
||||||
"cors": "~2.8.5",
|
"cors": "~2.8.5",
|
||||||
"debug": "~4.3.2",
|
"debug": "~4.3.2",
|
||||||
"engine.io": "~6.6.0",
|
"engine.io": "~6.5.2",
|
||||||
"socket.io-adapter": "~2.5.2",
|
"socket.io-adapter": "~2.5.2",
|
||||||
"socket.io-parser": "~4.2.4"
|
"socket.io-parser": "~4.2.4"
|
||||||
},
|
},
|
||||||
@ -1834,11 +1863,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@ -1850,9 +1879,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-adapter/node_modules/ms": {
|
"node_modules/socket.io-adapter/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter/node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-parser": {
|
"node_modules/socket.io-parser": {
|
||||||
"version": "4.2.4",
|
"version": "4.2.4",
|
||||||
@ -1867,11 +1916,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-parser/node_modules/debug": {
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@ -1883,16 +1932,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-parser/node_modules/ms": {
|
"node_modules/socket.io-parser/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/socket.io/node_modules/debug": {
|
"node_modules/socket.io/node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@ -1904,9 +1953,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io/node_modules/ms": {
|
"node_modules/socket.io/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/spawn-command": {
|
"node_modules/spawn-command": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
@ -1914,9 +1963,9 @@
|
|||||||
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
|
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ=="
|
||||||
},
|
},
|
||||||
"node_modules/ssh2": {
|
"node_modules/ssh2": {
|
||||||
"version": "1.16.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz",
|
||||||
"integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==",
|
"integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asn1": "^0.2.6",
|
"asn1": "^0.2.6",
|
||||||
@ -1926,8 +1975,8 @@
|
|||||||
"node": ">=10.16.0"
|
"node": ">=10.16.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"cpu-features": "~0.0.10",
|
"cpu-features": "~0.0.9",
|
||||||
"nan": "^2.20.0"
|
"nan": "^2.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
@ -2072,9 +2121,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||||
},
|
},
|
||||||
"node_modules/tweetnacl": {
|
"node_modules/tweetnacl": {
|
||||||
"version": "0.14.5",
|
"version": "0.14.5",
|
||||||
@ -2094,9 +2143,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.6.3",
|
"version": "5.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@ -2113,9 +2162,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "5.26.5",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||||
},
|
},
|
||||||
"node_modules/unpipe": {
|
"node_modules/unpipe": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -2125,6 +2174,20 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utf-8-validate": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -2169,26 +2232,6 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.17.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
|
||||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
@ -31,30 +31,9 @@ export class DokkuClient extends SSHSocketClient {
|
|||||||
|
|
||||||
// List all deployed Dokku apps
|
// List all deployed Dokku apps
|
||||||
async listApps(): Promise<string[]> {
|
async listApps(): Promise<string[]> {
|
||||||
const response = await this.sendCommand("--quiet apps:list")
|
const response = await this.sendCommand("apps:list")
|
||||||
return response.output.split("\n")
|
// Split the output by newline and remove the header
|
||||||
}
|
return response.output.split("\n").slice(1)
|
||||||
|
|
||||||
// Get the creation timestamp of an app
|
|
||||||
async getAppCreatedAt(appName: string): Promise<number> {
|
|
||||||
const response = await this.sendCommand(
|
|
||||||
`apps:report --app-created-at ${appName}`
|
|
||||||
)
|
|
||||||
const createdAt = parseInt(response.output.trim(), 10)
|
|
||||||
|
|
||||||
if (isNaN(createdAt)) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to retrieve creation timestamp for app ${appName}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if an app exists
|
|
||||||
async appExists(appName: string): Promise<boolean> {
|
|
||||||
const response = await this.sendCommand(`apps:exists ${appName}`)
|
|
||||||
return response.output.includes("App") === false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,11 +92,7 @@ export class FileManager {
|
|||||||
|
|
||||||
// Convert local file path to remote path
|
// Convert local file path to remote path
|
||||||
private getRemoteFileId(localId: string): string {
|
private getRemoteFileId(localId: string): string {
|
||||||
return [
|
return `projects/${this.sandboxId}${localId}`
|
||||||
"projects",
|
|
||||||
this.sandboxId,
|
|
||||||
localId.startsWith("/") ? localId : localId,
|
|
||||||
].join("/")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert remote file path to local file path
|
// Convert remote file path to local file path
|
||||||
@ -330,15 +326,13 @@ export class FileManager {
|
|||||||
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
|
if (Buffer.byteLength(body, "utf-8") > MAX_BODY_SIZE) {
|
||||||
throw new Error("File size too large. Please reduce the file size.")
|
throw new Error("File size too large. Please reduce the file size.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to remote storage
|
|
||||||
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
|
await RemoteFileStorage.saveFile(this.getRemoteFileId(fileId), body)
|
||||||
|
|
||||||
// Update local file data cache
|
|
||||||
let file = this.fileData.find((f) => f.id === fileId)
|
let file = this.fileData.find((f) => f.id === fileId)
|
||||||
if (file) {
|
if (file) {
|
||||||
file.data = body
|
file.data = body
|
||||||
} else {
|
} else {
|
||||||
|
// If the file wasn't in our cache, add it
|
||||||
file = {
|
file = {
|
||||||
id: fileId,
|
id: fileId,
|
||||||
data: body,
|
data: body,
|
||||||
@ -346,49 +340,7 @@ export class FileManager {
|
|||||||
this.fileData.push(file)
|
this.fileData.push(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to sandbox filesystem
|
await this.sandbox.files.write(path.posix.join(this.dirName, fileId), body)
|
||||||
const filePath = path.posix.join(this.dirName, fileId)
|
|
||||||
await this.sandbox.files.write(filePath, body)
|
|
||||||
|
|
||||||
// Instead of updating the entire file structure, just ensure this file exists in it
|
|
||||||
const parts = fileId.split('/').filter(Boolean)
|
|
||||||
let current = this.files
|
|
||||||
let currentPath = ''
|
|
||||||
|
|
||||||
// Navigate/create the path to the file
|
|
||||||
for (let i = 0; i < parts.length - 1; i++) {
|
|
||||||
currentPath += '/' + parts[i]
|
|
||||||
let folder = current.find(
|
|
||||||
(f) => f.type === 'folder' && f.name === parts[i]
|
|
||||||
) as TFolder
|
|
||||||
|
|
||||||
if (!folder) {
|
|
||||||
folder = {
|
|
||||||
id: currentPath,
|
|
||||||
type: 'folder',
|
|
||||||
name: parts[i],
|
|
||||||
children: [],
|
|
||||||
}
|
|
||||||
current.push(folder)
|
|
||||||
}
|
|
||||||
current = folder.children
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add/update the file in the structure if it doesn't exist
|
|
||||||
const fileName = parts[parts.length - 1]
|
|
||||||
const existingFile = current.find(
|
|
||||||
(f) => f.type === 'file' && f.name === fileName
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!existingFile) {
|
|
||||||
current.push({
|
|
||||||
id: fileId,
|
|
||||||
type: 'file',
|
|
||||||
name: fileName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshFileList?.(this.files)
|
|
||||||
this.fixPermissions()
|
this.fixPermissions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -71,13 +71,7 @@ export class Sandbox {
|
|||||||
} else {
|
} else {
|
||||||
console.log("Creating container", this.sandboxId)
|
console.log("Creating container", this.sandboxId)
|
||||||
// Create a new container with a specified template and timeout
|
// Create a new container with a specified template and timeout
|
||||||
const templateTypes = [
|
const templateTypes = ["vanillajs", "reactjs", "nextjs", "streamlit"]
|
||||||
"vanillajs",
|
|
||||||
"reactjs",
|
|
||||||
"nextjs",
|
|
||||||
"streamlit",
|
|
||||||
"php",
|
|
||||||
]
|
|
||||||
const template = templateTypes.includes(this.type)
|
const template = templateTypes.includes(this.type)
|
||||||
? `gitwit-${this.type}`
|
? `gitwit-${this.type}`
|
||||||
: `base`
|
: `base`
|
||||||
@ -156,40 +150,6 @@ export class Sandbox {
|
|||||||
return { success: true, apps: await this.dokkuClient.listApps() }
|
return { success: true, apps: await this.dokkuClient.listApps() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle getting app creation timestamp
|
|
||||||
const handleGetAppCreatedAt: SocketHandler = async ({ appName }) => {
|
|
||||||
if (!this.dokkuClient)
|
|
||||||
throw new Error(
|
|
||||||
"Failed to retrieve app creation timestamp: No Dokku client"
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
createdAt: await this.dokkuClient.getAppCreatedAt(appName),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle checking if an app exists
|
|
||||||
const handleAppExists: SocketHandler = async ({ appName }) => {
|
|
||||||
if (!this.dokkuClient) {
|
|
||||||
console.log("Failed to check app existence: No Dokku client")
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!this.dokkuClient.isConnected) {
|
|
||||||
console.log(
|
|
||||||
"Failed to check app existence: The Dokku client is not connected"
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
exists: await this.dokkuClient.appExists(appName),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle deploying code
|
// Handle deploying code
|
||||||
const handleDeploy: SocketHandler = async (_: any) => {
|
const handleDeploy: SocketHandler = async (_: any) => {
|
||||||
if (!this.gitClient) throw Error("No git client")
|
if (!this.gitClient) throw Error("No git client")
|
||||||
@ -287,9 +247,7 @@ export class Sandbox {
|
|||||||
getFolder: handleGetFolder,
|
getFolder: handleGetFolder,
|
||||||
saveFile: handleSaveFile,
|
saveFile: handleSaveFile,
|
||||||
moveFile: handleMoveFile,
|
moveFile: handleMoveFile,
|
||||||
listApps: handleListApps,
|
list: handleListApps,
|
||||||
getAppCreatedAt: handleGetAppCreatedAt,
|
|
||||||
getAppExists: handleAppExists,
|
|
||||||
deploy: handleDeploy,
|
deploy: handleDeploy,
|
||||||
createFile: handleCreateFile,
|
createFile: handleCreateFile,
|
||||||
createFolder: handleCreateFolder,
|
createFolder: handleCreateFolder,
|
||||||
|
@ -146,8 +146,6 @@ io.on("connection", async (socket) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.emit("ready")
|
|
||||||
|
|
||||||
// Handle disconnection event
|
// Handle disconnection event
|
||||||
socket.on("disconnect", async () => {
|
socket.on("disconnect", async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -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=
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
import ProfilePage from "@/components/profile"
|
|
||||||
import ProfileNavbar from "@/components/profile/navbar"
|
|
||||||
import { SandboxWithLiked, User } from "@/lib/types"
|
|
||||||
import { currentUser } from "@clerk/nextjs"
|
|
||||||
import { notFound } from "next/navigation"
|
|
||||||
|
|
||||||
export default async function Page({
|
|
||||||
params: { username: rawUsername },
|
|
||||||
}: {
|
|
||||||
params: { username: string }
|
|
||||||
}) {
|
|
||||||
const username = decodeURIComponent(rawUsername).replace("@", "")
|
|
||||||
const loggedInClerkUser = await currentUser()
|
|
||||||
|
|
||||||
const [profileOwnerResponse, loggedInUserResponse] = await Promise.all([
|
|
||||||
fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?username=${username}¤tUserId=${loggedInClerkUser?.id}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${loggedInClerkUser?.id}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
),
|
|
||||||
])
|
|
||||||
|
|
||||||
const profileOwner = (await profileOwnerResponse.json()) as User
|
|
||||||
const loggedInUser = (await loggedInUserResponse.json()) as User
|
|
||||||
|
|
||||||
if (!Boolean(profileOwner?.id)) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
const publicSandboxes: SandboxWithLiked[] = []
|
|
||||||
const privateSandboxes: SandboxWithLiked[] = []
|
|
||||||
|
|
||||||
profileOwner?.sandbox?.forEach((sandbox) => {
|
|
||||||
if (sandbox.visibility === "public") {
|
|
||||||
publicSandboxes.push(sandbox as SandboxWithLiked)
|
|
||||||
} else if (sandbox.visibility === "private") {
|
|
||||||
privateSandboxes.push(sandbox as SandboxWithLiked)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isUserLoggedIn = Boolean(loggedInUser?.id)
|
|
||||||
return (
|
|
||||||
<section>
|
|
||||||
<ProfileNavbar userData={loggedInUser} />
|
|
||||||
<ProfilePage
|
|
||||||
publicSandboxes={publicSandboxes}
|
|
||||||
privateSandboxes={
|
|
||||||
profileOwner?.id === loggedInUser.id ? privateSandboxes : []
|
|
||||||
}
|
|
||||||
profileOwner={profileOwner}
|
|
||||||
loggedInUser={isUserLoggedIn ? loggedInUser : null}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,59 +1,12 @@
|
|||||||
import {
|
|
||||||
ignoredFiles,
|
|
||||||
ignoredFolders,
|
|
||||||
} from "@/components/editor/AIChat/lib/ignored-paths"
|
|
||||||
import { templateConfigs } from "@/lib/templates"
|
|
||||||
import { TIERS } from "@/lib/tiers"
|
|
||||||
import { TFile, TFolder } from "@/lib/types"
|
|
||||||
import { Anthropic } from "@anthropic-ai/sdk"
|
|
||||||
import { currentUser } from "@clerk/nextjs"
|
import { currentUser } from "@clerk/nextjs"
|
||||||
|
import { Anthropic } from "@anthropic-ai/sdk"
|
||||||
|
import { TIERS } from "@/lib/tiers"
|
||||||
|
import { templateConfigs } from "@/lib/templates"
|
||||||
|
|
||||||
const anthropic = new Anthropic({
|
const anthropic = new Anthropic({
|
||||||
apiKey: process.env.ANTHROPIC_API_KEY!,
|
apiKey: process.env.ANTHROPIC_API_KEY!,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Format file structure for context
|
|
||||||
function formatFileStructure(
|
|
||||||
items: (TFile | TFolder)[] | undefined,
|
|
||||||
prefix = ""
|
|
||||||
): string {
|
|
||||||
if (!items || !Array.isArray(items)) {
|
|
||||||
return "No files available"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort items to show folders first, then files
|
|
||||||
const sortedItems = [...items].sort((a, b) => {
|
|
||||||
if (a.type === b.type) return a.name.localeCompare(b.name)
|
|
||||||
return a.type === "folder" ? -1 : 1
|
|
||||||
})
|
|
||||||
|
|
||||||
return sortedItems
|
|
||||||
.map((item) => {
|
|
||||||
if (
|
|
||||||
item.type === "file" &&
|
|
||||||
!ignoredFiles.some(
|
|
||||||
(pattern) =>
|
|
||||||
item.name.endsWith(pattern.replace("*", "")) ||
|
|
||||||
item.name === pattern
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return `${prefix}├── ${item.name}`
|
|
||||||
} else if (
|
|
||||||
item.type === "folder" &&
|
|
||||||
!ignoredFolders.some((folder) => folder === item.name)
|
|
||||||
) {
|
|
||||||
const folderContent = formatFileStructure(
|
|
||||||
item.children,
|
|
||||||
`${prefix}│ `
|
|
||||||
)
|
|
||||||
return `${prefix}├── ${item.name}/\n${folderContent}`
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const user = await currentUser()
|
const user = await currentUser()
|
||||||
@ -90,8 +43,7 @@ export async function POST(request: Request) {
|
|||||||
const userData = await dbUser.json()
|
const userData = await dbUser.json()
|
||||||
|
|
||||||
// Get tier settings
|
// Get tier settings
|
||||||
const tierSettings =
|
const tierSettings = TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
|
||||||
TIERS[userData.tier as keyof typeof TIERS] || TIERS.FREE
|
|
||||||
if (userData.generations >= tierSettings.generations) {
|
if (userData.generations >= tierSettings.generations) {
|
||||||
return new Response(
|
return new Response(
|
||||||
`AI generation limit reached for your ${userData.tier || "FREE"} tier`,
|
`AI generation limit reached for your ${userData.tier || "FREE"} tier`,
|
||||||
@ -106,32 +58,27 @@ export async function POST(request: Request) {
|
|||||||
isEditMode,
|
isEditMode,
|
||||||
fileName,
|
fileName,
|
||||||
line,
|
line,
|
||||||
templateType,
|
templateType
|
||||||
files,
|
|
||||||
projectName,
|
|
||||||
} = await request.json()
|
} = await request.json()
|
||||||
|
|
||||||
// Get template configuration
|
// Get template configuration
|
||||||
const templateConfig = templateConfigs[templateType]
|
const templateConfig = templateConfigs[templateType]
|
||||||
|
|
||||||
// Create template context
|
// Create template context
|
||||||
const templateContext = templateConfig
|
const templateContext = templateConfig ? `
|
||||||
? `
|
|
||||||
Project Template: ${templateConfig.name}
|
Project Template: ${templateConfig.name}
|
||||||
|
|
||||||
Current File Structure:
|
File Structure:
|
||||||
${files ? formatFileStructure(files) : "No files available"}
|
${Object.entries(templateConfig.fileStructure)
|
||||||
|
.map(([path, info]) => `${path} - ${info.description}`)
|
||||||
|
.join('\n')}
|
||||||
|
|
||||||
Conventions:
|
Conventions:
|
||||||
${templateConfig.conventions.join("\n")}
|
${templateConfig.conventions.join('\n')}
|
||||||
|
|
||||||
Dependencies:
|
Dependencies:
|
||||||
${JSON.stringify(templateConfig.dependencies, null, 2)}
|
${JSON.stringify(templateConfig.dependencies, null, 2)}
|
||||||
|
` : ''
|
||||||
Scripts:
|
|
||||||
${JSON.stringify(templateConfig.scripts, null, 2)}
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
|
|
||||||
// Create system message based on mode
|
// Create system message based on mode
|
||||||
let systemMessage
|
let systemMessage
|
||||||
@ -153,23 +100,13 @@ Instructions: ${messages[0].content}
|
|||||||
|
|
||||||
Respond only with the modified code that can directly replace the existing code.`
|
Respond only with the modified code that can directly replace the existing code.`
|
||||||
} else {
|
} else {
|
||||||
systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. When providing code:
|
systemMessage = `You are an intelligent programming assistant for a ${templateType} project. Please respond to the following request concisely. If your response includes code, please format it using triple backticks (\`\`\`) with the appropriate language identifier. For example:
|
||||||
|
|
||||||
1. Format it using triple backticks (\`\`\`) with the appropriate language identifier.
|
\`\`\`python
|
||||||
2. Always specify the complete file path in the format:
|
print("Hello, World!")
|
||||||
${projectName}/filepath/to/file.ext
|
\`\`\`
|
||||||
|
|
||||||
3. If creating a new file, specify the path as:
|
Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
||||||
${projectName}/filepath/to/file.ext (new file)
|
|
||||||
|
|
||||||
4. Format your code blocks as:
|
|
||||||
|
|
||||||
${projectName}/filepath/to/file.ext
|
|
||||||
\`\`\`language
|
|
||||||
code here
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
If multiple files are involved, repeat the format for each file. Provide a clear and concise explanation along with any code snippets. Keep your response brief and to the point.
|
|
||||||
|
|
||||||
This is the project template:
|
This is the project template:
|
||||||
${templateContext}
|
${templateContext}
|
||||||
@ -209,10 +146,7 @@ ${activeFileContent ? `Active File Content:\n${activeFileContent}\n` : ""}`
|
|||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
for await (const chunk of stream) {
|
for await (const chunk of stream) {
|
||||||
if (
|
if (chunk.type === "content_block_delta" && chunk.delta.type === "text_delta") {
|
||||||
chunk.type === "content_block_delta" &&
|
|
||||||
chunk.delta.type === "text_delta"
|
|
||||||
) {
|
|
||||||
controller.enqueue(encoder.encode(chunk.delta.text))
|
controller.enqueue(encoder.encode(chunk.delta.text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import OpenAI from "openai"
|
|
||||||
|
|
||||||
const openai = new OpenAI({
|
|
||||||
apiKey: process.env.OPENAI_API_KEY,
|
|
||||||
})
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const { originalCode, newCode, fileName } = await request.json()
|
|
||||||
|
|
||||||
const systemPrompt = `You are a code merging assistant. Your task is to merge the new code snippet with the original file content while:
|
|
||||||
1. Preserving the original file's functionality
|
|
||||||
2. Ensuring proper integration of the new code
|
|
||||||
3. Maintaining consistent style and formatting
|
|
||||||
4. Resolving any potential conflicts
|
|
||||||
5. Output ONLY the raw code without any:
|
|
||||||
- Code fence markers (\`\`\`)
|
|
||||||
- Language identifiers (typescript, javascript, etc.)
|
|
||||||
- Explanations or comments
|
|
||||||
- Markdown formatting
|
|
||||||
|
|
||||||
The output should be the exact code that will replace the existing code, nothing more and nothing less.
|
|
||||||
|
|
||||||
Important: When merging, preserve the original code structure as much as possible. Only make necessary changes to integrate the new code while maintaining the original code's organization and style.`
|
|
||||||
|
|
||||||
const mergedCode = `Original file (${fileName}):\n${originalCode}\n\nNew code to merge:\n${newCode}`
|
|
||||||
|
|
||||||
const response = await openai.chat.completions.create({
|
|
||||||
model: "gpt-4o",
|
|
||||||
messages: [
|
|
||||||
{ role: "system", content: systemPrompt },
|
|
||||||
{ role: "user", content: mergedCode },
|
|
||||||
],
|
|
||||||
prediction: {
|
|
||||||
type: "content",
|
|
||||||
content: mergedCode,
|
|
||||||
},
|
|
||||||
stream: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Clean and stream response
|
|
||||||
const encoder = new TextEncoder()
|
|
||||||
return new Response(
|
|
||||||
new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
let buffer = ""
|
|
||||||
for await (const chunk of response) {
|
|
||||||
if (chunk.choices[0]?.delta?.content) {
|
|
||||||
buffer += chunk.choices[0].delta.content
|
|
||||||
// Clean any code fence markers that might appear in the stream
|
|
||||||
const cleanedContent = buffer
|
|
||||||
.replace(/^```[\w-]*\n|```\s*$/gm, "") // Remove code fences
|
|
||||||
.replace(/^(javascript|typescript|python|html|css)\n/gm, "") // Remove language identifiers
|
|
||||||
controller.enqueue(encoder.encode(cleanedContent))
|
|
||||||
buffer = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
controller.close()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Merge error:", error)
|
|
||||||
return new Response(
|
|
||||||
error instanceof Error ? error.message : "Failed to merge code",
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
BIN
frontend/app/favicon.ico
Normal file
BIN
frontend/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@ -102,27 +102,26 @@
|
|||||||
.light .gradient-button-bg {
|
.light .gradient-button-bg {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
circle at top,
|
circle at top,
|
||||||
#f5f5f5 0%,
|
#262626 0%,
|
||||||
/* Very light gray */ #e0e0e0 50% /* Soft gray */
|
#f5f5f5 50%
|
||||||
);
|
); /* Dark gray -> Light gray */
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .gradient-button {
|
.light .gradient-button {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
circle at bottom,
|
circle at bottom,
|
||||||
hsl(0, 0%, 85%) -10%,
|
hsl(0, 10%, 25%) -10%,
|
||||||
/* Slightly darker gray */ hsl(0, 0%, 95%) 50% /* Very soft light gray */
|
#9d9d9d 50%
|
||||||
);
|
); /* Light gray -> Almost white */
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .gradient-button-bg > div:hover {
|
.light .gradient-button-bg > div:hover {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
circle at bottom,
|
circle at bottom,
|
||||||
hsl(0, 0%, 80%) -10%,
|
hsl(0, 10%, 25%) -10%,
|
||||||
/* Slightly darker gray for hover */ hsl(0, 0%, 90%) 80% /* Softer gray */
|
#9d9d9d 80%
|
||||||
);
|
); /* Light gray -> Almost white */
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-decoration::before {
|
.inline-decoration::before {
|
||||||
content: "Generate";
|
content: "Generate";
|
||||||
color: #525252;
|
color: #525252;
|
||||||
@ -176,23 +175,3 @@
|
|||||||
.tab-scroll::-webkit-scrollbar {
|
.tab-scroll::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.added-line-decoration {
|
|
||||||
background-color: rgba(0, 255, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.removed-line-decoration {
|
|
||||||
background-color: rgba(255, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.added-line-glyph {
|
|
||||||
background-color: #28a745;
|
|
||||||
width: 4px !important;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.removed-line-glyph {
|
|
||||||
background-color: #dc3545;
|
|
||||||
width: 4px !important;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 17 KiB |
@ -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({
|
||||||
|
@ -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 |
@ -8,20 +8,15 @@ import DashboardNavbarSearch from "./search"
|
|||||||
|
|
||||||
export default function DashboardNavbar({ userData }: { userData: User }) {
|
export default function DashboardNavbar({ userData }: { userData: User }) {
|
||||||
return (
|
return (
|
||||||
<div className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
|
<div className="h-16 px-4 w-full flex items-center justify-between border-b border-border">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-4">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
|
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
|
||||||
>
|
>
|
||||||
<Image src={Logo} alt="Logo" width={36} height={36} />
|
<Image src={Logo} alt="Logo" width={36} height={36} />
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="text-xl">
|
<div className="text-sm font-medium flex items-center">Sandbox</div>
|
||||||
<span className="font-semibold">Sandbox</span>{" "}
|
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
|
||||||
by gitwit
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<DashboardNavbarSearch />
|
<DashboardNavbarSearch />
|
||||||
|
@ -11,13 +11,13 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
|
||||||
export default function ProjectCardDropdown({
|
export default function ProjectCardDropdown({
|
||||||
visibility,
|
sandbox,
|
||||||
onVisibilityChange,
|
onVisibilityChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: {
|
}: {
|
||||||
visibility: Sandbox["visibility"]
|
sandbox: Sandbox
|
||||||
onVisibilityChange: () => void
|
onVisibilityChange: (sandbox: Sandbox) => void
|
||||||
onDelete: () => void
|
onDelete: (sandbox: Sandbox) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
@ -26,7 +26,7 @@ export default function ProjectCardDropdown({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
className="h-6 w-6 z-10 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
|
className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
|
||||||
>
|
>
|
||||||
<Ellipsis className="w-4 h-4" />
|
<Ellipsis className="w-4 h-4" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@ -34,11 +34,11 @@ export default function ProjectCardDropdown({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onVisibilityChange()
|
onVisibilityChange(sandbox)
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{visibility === "public" ? (
|
{sandbox.visibility === "public" ? (
|
||||||
<>
|
<>
|
||||||
<Lock className="mr-2 h-4 w-4" />
|
<Lock className="mr-2 h-4 w-4" />
|
||||||
<span>Make Private</span>
|
<span>Make Private</span>
|
||||||
@ -53,7 +53,7 @@ export default function ProjectCardDropdown({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onDelete()
|
onDelete(sandbox)
|
||||||
}}
|
}}
|
||||||
className="!text-destructive cursor-pointer"
|
className="!text-destructive cursor-pointer"
|
||||||
>
|
>
|
||||||
|
@ -1,235 +1,59 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Card } from "@/components/ui/card"
|
import { Card } from "@/components/ui/card"
|
||||||
import { toggleLike } from "@/lib/actions"
|
|
||||||
import { projectTemplates } from "@/lib/data"
|
import { projectTemplates } from "@/lib/data"
|
||||||
import { Sandbox } from "@/lib/types"
|
import { Sandbox } from "@/lib/types"
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { useUser } from "@clerk/nextjs"
|
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
import { Clock, Eye, Globe, Heart, Lock } from "lucide-react"
|
import { Clock, Globe, Lock } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import {
|
import { useEffect, useState } from "react"
|
||||||
memo,
|
|
||||||
MouseEventHandler,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useOptimistic,
|
|
||||||
useState,
|
|
||||||
useTransition,
|
|
||||||
} from "react"
|
|
||||||
import ProjectCardDropdown from "./dropdown"
|
import ProjectCardDropdown from "./dropdown"
|
||||||
import { CanvasRevealEffect } from "./revealEffect"
|
|
||||||
|
|
||||||
type BaseProjectCardProps = {
|
export default function ProjectCard({
|
||||||
id: string
|
children,
|
||||||
name: string
|
sandbox,
|
||||||
type: string
|
onVisibilityChange,
|
||||||
visibility: "public" | "private"
|
onDelete,
|
||||||
createdAt: Date
|
deletingId,
|
||||||
likeCount: number
|
}: {
|
||||||
liked?: boolean
|
children?: React.ReactNode
|
||||||
viewCount: number
|
sandbox: Sandbox
|
||||||
}
|
onVisibilityChange: (sandbox: Sandbox) => void
|
||||||
|
onDelete: (sandbox: Sandbox) => void
|
||||||
type AuthenticatedProjectCardProps = BaseProjectCardProps & {
|
|
||||||
isAuthenticated: true
|
|
||||||
onVisibilityChange: (
|
|
||||||
sandbox: Pick<Sandbox, "id" | "name" | "visibility">
|
|
||||||
) => void
|
|
||||||
onDelete: (sandbox: Pick<Sandbox, "id" | "name">) => void
|
|
||||||
deletingId: string
|
deletingId: string
|
||||||
}
|
}) {
|
||||||
|
|
||||||
type UnauthenticatedProjectCardProps = BaseProjectCardProps & {
|
|
||||||
isAuthenticated: false
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectCardProps =
|
|
||||||
| AuthenticatedProjectCardProps
|
|
||||||
| UnauthenticatedProjectCardProps
|
|
||||||
|
|
||||||
const StatItem = memo(({ icon: Icon, value }: { icon: any; value: number }) => (
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<Icon className="size-4" />
|
|
||||||
<span className="text-xs">{value}</span>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
|
|
||||||
StatItem.displayName = "StatItem"
|
|
||||||
|
|
||||||
const formatDate = (date: Date): string => {
|
|
||||||
const now = new Date()
|
|
||||||
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / 60000)
|
|
||||||
|
|
||||||
if (diffInMinutes < 1) return "Now"
|
|
||||||
if (diffInMinutes < 60) return `${diffInMinutes}m ago`
|
|
||||||
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`
|
|
||||||
return `${Math.floor(diffInMinutes / 1440)}d ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectMetadata = memo(
|
|
||||||
({
|
|
||||||
id,
|
|
||||||
visibility,
|
|
||||||
createdAt,
|
|
||||||
likeCount,
|
|
||||||
liked,
|
|
||||||
viewCount,
|
|
||||||
}: Pick<
|
|
||||||
BaseProjectCardProps,
|
|
||||||
"visibility" | "createdAt" | "likeCount" | "liked" | "viewCount" | "id"
|
|
||||||
>) => {
|
|
||||||
const { user } = useUser()
|
|
||||||
const [date, setDate] = useState<string>()
|
|
||||||
const Icon = visibility === "private" ? Lock : Globe
|
|
||||||
useEffect(() => {
|
|
||||||
setDate(formatDate(new Date(createdAt)))
|
|
||||||
}, [createdAt])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col text-muted-foreground space-y-2 text-sm z-10">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Icon className="size-4" />
|
|
||||||
<span className="text-xs">
|
|
||||||
{visibility === "private" ? "Private" : "Public"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Clock className="size-4" /> <span className="text-xs">{date}</span>
|
|
||||||
</div>
|
|
||||||
<LikeButton
|
|
||||||
sandboxId={id}
|
|
||||||
initialIsLiked={!!liked}
|
|
||||||
initialLikeCount={likeCount}
|
|
||||||
userId={user?.id ?? null}
|
|
||||||
/>
|
|
||||||
<StatItem icon={Eye} value={viewCount} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ProjectMetadata.displayName = "ProjectMetadata"
|
|
||||||
|
|
||||||
interface LikeButtonProps {
|
|
||||||
sandboxId: string
|
|
||||||
userId: string | null
|
|
||||||
initialLikeCount: number
|
|
||||||
initialIsLiked: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LikeButton({
|
|
||||||
sandboxId,
|
|
||||||
userId,
|
|
||||||
initialLikeCount,
|
|
||||||
initialIsLiked,
|
|
||||||
}: LikeButtonProps) {
|
|
||||||
// Optimistic state for like status and count
|
|
||||||
const [{ isLiked, likeCount }, optimisticUpdateLike] = useOptimistic(
|
|
||||||
{ isLiked: initialIsLiked, likeCount: initialLikeCount },
|
|
||||||
(state, optimisticValue: boolean) => {
|
|
||||||
return {
|
|
||||||
isLiked: optimisticValue,
|
|
||||||
likeCount: state.likeCount + (optimisticValue ? 1 : -1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const [isPending, startTransition] = useTransition()
|
|
||||||
|
|
||||||
const handleLike: MouseEventHandler<HTMLButtonElement> = async (e) => {
|
|
||||||
e.stopPropagation() // Prevent click event from bubbling up which leads to navigation to /code/:id
|
|
||||||
if (!userId) return
|
|
||||||
|
|
||||||
startTransition(async () => {
|
|
||||||
const newLikeState = !isLiked
|
|
||||||
try {
|
|
||||||
optimisticUpdateLike(newLikeState)
|
|
||||||
await toggleLike(sandboxId, userId)
|
|
||||||
} catch (error) {
|
|
||||||
console.log("error", error)
|
|
||||||
optimisticUpdateLike(!newLikeState)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
disabled={!userId || isPending}
|
|
||||||
onClick={handleLike}
|
|
||||||
className="gap-1 px-1 rounded-full"
|
|
||||||
>
|
|
||||||
<Heart
|
|
||||||
className={cn("size-4", isLiked ? "stroke-red-500 fill-red-500" : "")}
|
|
||||||
/>
|
|
||||||
<span className="text-xs">{likeCount}</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function ProjectCardComponent({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
visibility,
|
|
||||||
createdAt,
|
|
||||||
likeCount,
|
|
||||||
viewCount,
|
|
||||||
...props
|
|
||||||
}: ProjectCardProps) {
|
|
||||||
const [hovered, setHovered] = useState(false)
|
const [hovered, setHovered] = useState(false)
|
||||||
|
const [date, setDate] = useState<string>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const projectIcon = useMemo(
|
useEffect(() => {
|
||||||
() =>
|
const createdAt = new Date(sandbox.createdAt)
|
||||||
projectTemplates.find((p) => p.id === type)?.icon ??
|
const now = new Date()
|
||||||
"/project-icons/node.svg",
|
const diffInMinutes = Math.floor(
|
||||||
[type]
|
(now.getTime() - createdAt.getTime()) / 60000
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleVisibilityChange = () => {
|
if (diffInMinutes < 1) {
|
||||||
if (props.isAuthenticated) {
|
setDate("Now")
|
||||||
props.onVisibilityChange({
|
} else if (diffInMinutes < 60) {
|
||||||
id,
|
setDate(`${diffInMinutes}m ago`)
|
||||||
name,
|
} else if (diffInMinutes < 1440) {
|
||||||
visibility,
|
setDate(`${Math.floor(diffInMinutes / 60)}h ago`)
|
||||||
})
|
} else {
|
||||||
|
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
|
||||||
}
|
}
|
||||||
}
|
}, [sandbox])
|
||||||
|
const projectIcon =
|
||||||
const handleDelete = () => {
|
projectTemplates.find((p) => p.id === sandbox.type)?.icon ??
|
||||||
if (props.isAuthenticated) {
|
"/project-icons/node.svg"
|
||||||
props.onDelete({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => router.push(`/code/${id}`)}
|
onClick={() => router.push(`/code/${sandbox.id}`)}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
className={`
|
className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`}
|
||||||
group/canvas-card p-4 h-48 flex flex-col justify-between items-start
|
|
||||||
hover:border-muted-foreground/50 relative overflow-hidden transition-all
|
|
||||||
${
|
|
||||||
props.isAuthenticated && props.deletingId === id
|
|
||||||
? "opacity-50 pointer-events-none cursor-events-none"
|
|
||||||
: "cursor-pointer"
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{hovered && (
|
{hovered && (
|
||||||
@ -238,64 +62,38 @@ function ProjectCardComponent({
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
className="h-full w-full absolute inset-0"
|
className="h-full w-full absolute inset-0"
|
||||||
>
|
>
|
||||||
<CanvasRevealEffect
|
{children}
|
||||||
animationSpeed={3}
|
|
||||||
containerClassName="bg-muted"
|
|
||||||
colors={colors[type]}
|
|
||||||
dotSize={2}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
||||||
<Image
|
<Image alt="" src={projectIcon} width={20} height={20} />
|
||||||
alt={`${type} project icon`}
|
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
|
||||||
src={projectIcon}
|
{sandbox.name}
|
||||||
width={20}
|
</div>
|
||||||
height={20}
|
<ProjectCardDropdown
|
||||||
|
sandbox={sandbox}
|
||||||
|
onVisibilityChange={onVisibilityChange}
|
||||||
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
<Link
|
|
||||||
href={`/code/${id}`}
|
|
||||||
className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden before:content-[''] before:absolute before:z-0 before:top-0 before:left-0 before:w-full before:h-full before:rounded-xl"
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Link>
|
|
||||||
{props.isAuthenticated && (
|
|
||||||
<ProjectCardDropdown
|
|
||||||
onVisibilityChange={handleVisibilityChange}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
visibility={visibility}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col text-muted-foreground space-y-0.5 text-sm z-10">
|
||||||
<ProjectMetadata
|
<div className="flex items-center">
|
||||||
visibility={visibility}
|
{sandbox.visibility === "private" ? (
|
||||||
createdAt={createdAt}
|
<>
|
||||||
likeCount={likeCount}
|
<Lock className="w-3 h-3 mr-2" /> Private
|
||||||
viewCount={viewCount}
|
</>
|
||||||
id={id}
|
) : (
|
||||||
liked={props.liked}
|
<>
|
||||||
/>
|
<Globe className="w-3 h-3 mr-2" /> Public
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Clock className="w-3 h-3 mr-2" /> {date}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectCardComponent.displayName = "ProjectCard"
|
|
||||||
|
|
||||||
const ProjectCard = memo(ProjectCardComponent)
|
|
||||||
|
|
||||||
export default ProjectCard
|
|
||||||
|
|
||||||
const colors: { [key: string]: number[][] } = {
|
|
||||||
react: [
|
|
||||||
[71, 207, 237],
|
|
||||||
[30, 126, 148],
|
|
||||||
],
|
|
||||||
node: [
|
|
||||||
[86, 184, 72],
|
|
||||||
[59, 112, 52],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
||||||
import { Sandbox } from "@/lib/types"
|
import { Sandbox } from "@/lib/types"
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import Link from "next/link"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import ProjectCard from "./projectCard"
|
import ProjectCard from "./projectCard"
|
||||||
|
import { CanvasRevealEffect } from "./projectCard/revealEffect"
|
||||||
|
|
||||||
const colors: { [key: string]: number[][] } = {
|
const colors: { [key: string]: number[][] } = {
|
||||||
react: [
|
react: [
|
||||||
@ -26,27 +28,11 @@ export default function DashboardProjects({
|
|||||||
}) {
|
}) {
|
||||||
const [deletingId, setDeletingId] = useState<string>("")
|
const [deletingId, setDeletingId] = useState<string>("")
|
||||||
|
|
||||||
const onVisibilityChange = useMemo(
|
const onDelete = async (sandbox: Sandbox) => {
|
||||||
() => async (sandbox: Pick<Sandbox, "id" | "name" | "visibility">) => {
|
setDeletingId(sandbox.id)
|
||||||
const newVisibility =
|
toast(`Project ${sandbox.name} deleted.`)
|
||||||
sandbox.visibility === "public" ? "private" : "public"
|
await deleteSandbox(sandbox.id)
|
||||||
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
|
}
|
||||||
await updateSandbox({
|
|
||||||
id: sandbox.id,
|
|
||||||
visibility: newVisibility,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onDelete = useMemo(
|
|
||||||
() => async (sandbox: Pick<Sandbox, "id" | "name">) => {
|
|
||||||
setDeletingId(sandbox.id)
|
|
||||||
toast(`Project ${sandbox.name} deleted.`)
|
|
||||||
await deleteSandbox(sandbox.id)
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (deletingId) {
|
if (deletingId) {
|
||||||
@ -54,6 +40,15 @@ export default function DashboardProjects({
|
|||||||
}
|
}
|
||||||
}, [sandboxes])
|
}, [sandboxes])
|
||||||
|
|
||||||
|
const onVisibilityChange = async (sandbox: Sandbox) => {
|
||||||
|
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
|
||||||
|
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
|
||||||
|
await updateSandbox({
|
||||||
|
id: sandbox.id,
|
||||||
|
visibility: newVisibility,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grow p-4 flex flex-col">
|
<div className="grow p-4 flex flex-col">
|
||||||
<div className="text-xl font-medium mb-8">
|
<div className="text-xl font-medium mb-8">
|
||||||
@ -69,14 +64,30 @@ export default function DashboardProjects({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ProjectCard
|
<Link
|
||||||
key={sandbox.id}
|
key={sandbox.id}
|
||||||
onVisibilityChange={onVisibilityChange}
|
href={`/code/${sandbox.id}`}
|
||||||
onDelete={onDelete}
|
className={`${
|
||||||
deletingId={deletingId}
|
deletingId === sandbox.id
|
||||||
isAuthenticated
|
? "pointer-events-none opacity-50 cursor-events-none"
|
||||||
{...sandbox}
|
: "cursor-pointer"
|
||||||
/>
|
} transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg`}
|
||||||
|
>
|
||||||
|
<ProjectCard
|
||||||
|
sandbox={sandbox}
|
||||||
|
onVisibilityChange={onVisibilityChange}
|
||||||
|
onDelete={onDelete}
|
||||||
|
deletingId={deletingId}
|
||||||
|
>
|
||||||
|
<CanvasRevealEffect
|
||||||
|
animationSpeed={3}
|
||||||
|
containerClassName="bg-black"
|
||||||
|
colors={colors[sandbox.type]}
|
||||||
|
dotSize={2}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
|
||||||
|
</ProjectCard>
|
||||||
|
</Link>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
import { Check, Loader2 } from "lucide-react"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import { Button } from "../../ui/button"
|
|
||||||
|
|
||||||
interface ApplyButtonProps {
|
|
||||||
code: string
|
|
||||||
activeFileName: string
|
|
||||||
activeFileContent: string
|
|
||||||
editorRef: { current: any }
|
|
||||||
onApply: (mergedCode: string, originalCode: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ApplyButton({
|
|
||||||
code,
|
|
||||||
activeFileName,
|
|
||||||
activeFileContent,
|
|
||||||
editorRef,
|
|
||||||
onApply,
|
|
||||||
}: ApplyButtonProps) {
|
|
||||||
const [isApplying, setIsApplying] = useState(false)
|
|
||||||
|
|
||||||
const handleApply = async () => {
|
|
||||||
setIsApplying(true)
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/merge", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
originalCode: activeFileContent,
|
|
||||||
newCode: String(code),
|
|
||||||
fileName: activeFileName,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(await response.text())
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = response.body?.getReader()
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
let mergedCode = ""
|
|
||||||
|
|
||||||
if (reader) {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
mergedCode += decoder.decode(value, { stream: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onApply(mergedCode.trim(), activeFileContent)
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error applying code:", error)
|
|
||||||
toast.error(
|
|
||||||
error instanceof Error ? error.message : "Failed to apply code changes"
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
setIsApplying(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
onClick={handleApply}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="p-1 h-6"
|
|
||||||
disabled={isApplying}
|
|
||||||
>
|
|
||||||
{isApplying ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
@ -13,13 +13,6 @@ export default function ChatMessage({
|
|||||||
setContext,
|
setContext,
|
||||||
setIsContextExpanded,
|
setIsContextExpanded,
|
||||||
socket,
|
socket,
|
||||||
handleApplyCode,
|
|
||||||
activeFileName,
|
|
||||||
activeFileContent,
|
|
||||||
editorRef,
|
|
||||||
mergeDecorationsCollection,
|
|
||||||
setMergeDecorationsCollection,
|
|
||||||
selectFile,
|
|
||||||
}: MessageProps) {
|
}: MessageProps) {
|
||||||
// State for expanded message index
|
// State for expanded message index
|
||||||
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
const [expandedMessageIndex, setExpandedMessageIndex] = useState<
|
||||||
@ -111,14 +104,7 @@ export default function ChatMessage({
|
|||||||
const components = createMarkdownComponents(
|
const components = createMarkdownComponents(
|
||||||
renderCopyButton,
|
renderCopyButton,
|
||||||
renderMarkdownElement,
|
renderMarkdownElement,
|
||||||
askAboutCode,
|
askAboutCode
|
||||||
activeFileName,
|
|
||||||
activeFileContent,
|
|
||||||
editorRef,
|
|
||||||
handleApplyCode,
|
|
||||||
selectFile,
|
|
||||||
mergeDecorationsCollection,
|
|
||||||
setMergeDecorationsCollection,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -126,8 +112,8 @@ export default function ChatMessage({
|
|||||||
<div
|
<div
|
||||||
className={`relative p-2 rounded-lg ${
|
className={`relative p-2 rounded-lg ${
|
||||||
message.role === "user"
|
message.role === "user"
|
||||||
? "bg-foreground text-background"
|
? "bg-[#262626] text-white"
|
||||||
: "bg-background text-foreground"
|
: "bg-transparent text-white"
|
||||||
} max-w-full`}
|
} max-w-full`}
|
||||||
>
|
>
|
||||||
{/* Render context tabs */}
|
{/* Render context tabs */}
|
||||||
@ -171,7 +157,7 @@ export default function ChatMessage({
|
|||||||
end: e.target.value.split("\n").length,
|
end: e.target.value.split("\n").length,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
className="w-full p-2 bg-[#1e1e1e] text-foreground font-mono text-sm rounded"
|
className="w-full p-2 bg-[#1e1e1e] text-white font-mono text-sm rounded"
|
||||||
rows={code.split("\n").length}
|
rows={code.split("\n").length}
|
||||||
style={{
|
style={{
|
||||||
resize: "vertical",
|
resize: "vertical",
|
||||||
@ -215,8 +201,7 @@ export default function ChatMessage({
|
|||||||
|
|
||||||
// Parse context to tabs for context tabs component
|
// Parse context to tabs for context tabs component
|
||||||
function parseContextToTabs(context: string) {
|
function parseContextToTabs(context: string) {
|
||||||
// Use specific regex patterns to avoid matching import statements
|
const sections = context.split(/(?=File |Code from )/)
|
||||||
const sections = context.split(/(?=File |Code from |Image \d{1,2}:)/)
|
|
||||||
return sections
|
return sections
|
||||||
.map((section, index) => {
|
.map((section, index) => {
|
||||||
const lines = section.trim().split("\n")
|
const lines = section.trim().split("\n")
|
||||||
@ -226,29 +211,16 @@ function parseContextToTabs(context: string) {
|
|||||||
// Remove code block markers for display
|
// Remove code block markers for display
|
||||||
content = content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
|
content = content.replace(/^```[\w-]*\n/, "").replace(/\n```$/, "")
|
||||||
|
|
||||||
// Determine the type of context
|
// Determine if the context is a file or code
|
||||||
const isFile = titleLine.startsWith("File ")
|
const isFile = titleLine.startsWith("File ")
|
||||||
const isImage = titleLine.startsWith("Image ")
|
const name = titleLine.replace(/^(File |Code from )/, "").replace(":", "")
|
||||||
const type = isFile ? "file" : isImage ? "image" : "code"
|
|
||||||
const name = titleLine
|
|
||||||
.replace(/^(File |Code from |Image )/, "")
|
|
||||||
.replace(":", "")
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
// Skip if the content is empty or if it's just an import statement
|
|
||||||
if (!content || content.trim().startsWith('from "')) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `context-${index}`,
|
id: `context-${index}`,
|
||||||
type: type as "file" | "code" | "image",
|
type: isFile ? ("file" as const) : ("code" as const),
|
||||||
name: name,
|
name: name,
|
||||||
content: content,
|
content: content,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(
|
.filter((tab) => tab.content.length > 0)
|
||||||
(tab): tab is NonNullable<typeof tab> =>
|
|
||||||
tab !== null && tab.content.length > 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
||||||
import { useSocket } from "@/context/SocketContext"
|
import { useSocket } from "@/context/SocketContext"
|
||||||
import { TFile } from "@/lib/types"
|
import { TFile } from "@/lib/types"
|
||||||
import { ChevronDown, X } from "lucide-react"
|
import { X, ChevronDown } from "lucide-react"
|
||||||
import { nanoid } from "nanoid"
|
import { nanoid } from "nanoid"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import LoadingDots from "../../ui/LoadingDots"
|
import LoadingDots from "../../ui/LoadingDots"
|
||||||
@ -19,11 +18,6 @@ export default function AIChat({
|
|||||||
lastCopiedRangeRef,
|
lastCopiedRangeRef,
|
||||||
files,
|
files,
|
||||||
templateType,
|
templateType,
|
||||||
handleApplyCode,
|
|
||||||
selectFile,
|
|
||||||
mergeDecorationsCollection,
|
|
||||||
setMergeDecorationsCollection,
|
|
||||||
projectName,
|
|
||||||
}: AIChatProps) {
|
}: AIChatProps) {
|
||||||
// Initialize socket and messages
|
// Initialize socket and messages
|
||||||
const { socket } = useSocket()
|
const { socket } = useSocket()
|
||||||
@ -81,8 +75,8 @@ export default function AIChat({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = chatContainerRef.current
|
const container = chatContainerRef.current
|
||||||
if (container) {
|
if (container) {
|
||||||
container.addEventListener("scroll", handleScroll)
|
container.addEventListener('scroll', handleScroll)
|
||||||
return () => container.removeEventListener("scroll", handleScroll)
|
return () => container.removeEventListener('scroll', handleScroll)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -131,8 +125,6 @@ export default function AIChat({
|
|||||||
} else if (tab.type === "code") {
|
} else if (tab.type === "code") {
|
||||||
const cleanContent = formatCodeContent(tab.content)
|
const cleanContent = formatCodeContent(tab.content)
|
||||||
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
|
return `Code from ${tab.name}:\n\`\`\`typescript\n${cleanContent}\n\`\`\``
|
||||||
} else if (tab.type === "image") {
|
|
||||||
return `Image ${tab.name}:\n${tab.content}`
|
|
||||||
}
|
}
|
||||||
return `${tab.name}:\n${tab.content}`
|
return `${tab.name}:\n${tab.content}`
|
||||||
})
|
})
|
||||||
@ -154,10 +146,10 @@ export default function AIChat({
|
|||||||
abortControllerRef,
|
abortControllerRef,
|
||||||
activeFileContent,
|
activeFileContent,
|
||||||
false,
|
false,
|
||||||
templateType,
|
templateType
|
||||||
files,
|
|
||||||
projectName
|
|
||||||
)
|
)
|
||||||
|
// Clear context tabs after sending
|
||||||
|
setContextTabs([])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set context for the chat
|
// Set context for the chat
|
||||||
@ -175,22 +167,6 @@ export default function AIChat({
|
|||||||
addContextTab("code", name, context, range)
|
addContextTab("code", name, context, range)
|
||||||
}
|
}
|
||||||
|
|
||||||
// update context tabs when file contents change
|
|
||||||
useEffect(() => {
|
|
||||||
setContextTabs((prevTabs) =>
|
|
||||||
prevTabs.map((tab) => {
|
|
||||||
if (tab.type === "file" && tab.name === activeFileName) {
|
|
||||||
const fileExt = tab.name.split(".").pop() || "txt"
|
|
||||||
return {
|
|
||||||
...tab,
|
|
||||||
content: `\`\`\`${fileExt}\n${activeFileContent}\n\`\`\``,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tab
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}, [activeFileContent, activeFileName])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen w-full">
|
<div className="flex flex-col h-screen w-full">
|
||||||
<div className="flex justify-between items-center p-2 border-b">
|
<div className="flex justify-between items-center p-2 border-b">
|
||||||
@ -209,9 +185,9 @@ export default function AIChat({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea
|
<div
|
||||||
ref={chatContainerRef}
|
ref={chatContainerRef}
|
||||||
className="flex-grow p-4 space-y-4 relative"
|
className="flex-grow overflow-y-auto p-4 space-y-4 relative"
|
||||||
>
|
>
|
||||||
{messages.map((message, messageIndex) => (
|
{messages.map((message, messageIndex) => (
|
||||||
// Render chat message component for each message
|
// Render chat message component for each message
|
||||||
@ -221,13 +197,6 @@ export default function AIChat({
|
|||||||
setContext={setContext}
|
setContext={setContext}
|
||||||
setIsContextExpanded={setIsContextExpanded}
|
setIsContextExpanded={setIsContextExpanded}
|
||||||
socket={socket}
|
socket={socket}
|
||||||
handleApplyCode={handleApplyCode}
|
|
||||||
activeFileName={activeFileName}
|
|
||||||
activeFileContent={activeFileContent}
|
|
||||||
editorRef={editorRef}
|
|
||||||
mergeDecorationsCollection={mergeDecorationsCollection}
|
|
||||||
setMergeDecorationsCollection={setMergeDecorationsCollection}
|
|
||||||
selectFile={selectFile}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isLoading && <LoadingDots />}
|
{isLoading && <LoadingDots />}
|
||||||
@ -242,7 +211,7 @@ export default function AIChat({
|
|||||||
<ChevronDown className="h-5 w-5" />
|
<ChevronDown className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</div>
|
||||||
<div className="p-4 border-t mb-14">
|
<div className="p-4 border-t mb-14">
|
||||||
{/* Render context tabs component */}
|
{/* Render context tabs component */}
|
||||||
<ContextTabs
|
<ContextTabs
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { TFile, TFolder } from "@/lib/types"
|
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
// Stringify content for chat message component
|
// Stringify content for chat message component
|
||||||
@ -92,9 +91,7 @@ export const handleSend = async (
|
|||||||
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
abortControllerRef: React.MutableRefObject<AbortController | null>,
|
||||||
activeFileContent: string,
|
activeFileContent: string,
|
||||||
isEditMode: boolean = false,
|
isEditMode: boolean = false,
|
||||||
templateType: string,
|
templateType: string
|
||||||
files: (TFile | TFolder)[],
|
|
||||||
projectName: string
|
|
||||||
) => {
|
) => {
|
||||||
// Return if input is empty and context is null
|
// Return if input is empty and context is null
|
||||||
if (input.trim() === "" && !context) return
|
if (input.trim() === "" && !context) return
|
||||||
@ -134,22 +131,22 @@ export const handleSend = async (
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// Fetch AI response for chat message component
|
// Fetch AI response for chat message component
|
||||||
const response = await fetch("/api/ai", {
|
const response = await fetch("/api/ai",
|
||||||
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: anthropicMessages,
|
messages: anthropicMessages,
|
||||||
context: context || undefined,
|
context: context || undefined,
|
||||||
activeFileContent: activeFileContent,
|
activeFileContent: activeFileContent,
|
||||||
isEditMode: isEditMode,
|
isEditMode: isEditMode,
|
||||||
templateType: templateType,
|
templateType: templateType,
|
||||||
files: files,
|
}),
|
||||||
projectName: projectName,
|
signal: abortControllerRef.current.signal,
|
||||||
}),
|
}
|
||||||
signal: abortControllerRef.current.signal,
|
)
|
||||||
})
|
|
||||||
|
|
||||||
// Throw error if response is not ok
|
// Throw error if response is not ok
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -204,8 +201,7 @@ export const handleSend = async (
|
|||||||
console.error("Error fetching AI response:", error)
|
console.error("Error fetching AI response:", error)
|
||||||
const errorMessage = {
|
const errorMessage = {
|
||||||
role: "assistant" as const,
|
role: "assistant" as const,
|
||||||
content:
|
content: error.message || "Sorry, I encountered an error. Please try again.",
|
||||||
error.message || "Sorry, I encountered an error. Please try again.",
|
|
||||||
}
|
}
|
||||||
setMessages((prev) => [...prev, errorMessage])
|
setMessages((prev) => [...prev, errorMessage])
|
||||||
}
|
}
|
||||||
@ -243,11 +239,3 @@ export const looksLikeCode = (text: string): boolean => {
|
|||||||
|
|
||||||
return codeIndicators.some((pattern) => pattern.test(text))
|
return codeIndicators.some((pattern) => pattern.test(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this new function after looksLikeCode function
|
|
||||||
export const isFilePath = (text: string): boolean => {
|
|
||||||
// Match patterns like next/styles/SignIn.module.css or path/to/file.ext (new file)
|
|
||||||
const pattern =
|
|
||||||
/^(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+(\s+\(new file\))?$/
|
|
||||||
return pattern.test(text)
|
|
||||||
}
|
|
||||||
|
@ -1,26 +1,15 @@
|
|||||||
import { useSocket } from "@/context/SocketContext"
|
import { CornerUpLeft } from "lucide-react"
|
||||||
import { TTab } from "@/lib/types"
|
|
||||||
import { Check, CornerUpLeft, FileText, X } from "lucide-react"
|
|
||||||
import monaco from "monaco-editor"
|
|
||||||
import { Components } from "react-markdown"
|
import { Components } from "react-markdown"
|
||||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||||
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||||
import { Button } from "../../../ui/button"
|
import { Button } from "../../../ui/button"
|
||||||
import ApplyButton from "../ApplyButton"
|
import { stringifyContent } from "./chatUtils"
|
||||||
import { isFilePath, stringifyContent } from "./chatUtils"
|
|
||||||
|
|
||||||
// Create markdown components for chat message component
|
// Create markdown components for chat message component
|
||||||
export const createMarkdownComponents = (
|
export const createMarkdownComponents = (
|
||||||
renderCopyButton: (text: any) => JSX.Element,
|
renderCopyButton: (text: any) => JSX.Element,
|
||||||
renderMarkdownElement: (props: any) => JSX.Element,
|
renderMarkdownElement: (props: any) => JSX.Element,
|
||||||
askAboutCode: (code: any) => void,
|
askAboutCode: (code: any) => void
|
||||||
activeFileName: string,
|
|
||||||
activeFileContent: string,
|
|
||||||
editorRef: any,
|
|
||||||
handleApplyCode: (mergedCode: string, originalCode: string) => void,
|
|
||||||
selectFile: (tab: TTab) => void,
|
|
||||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection,
|
|
||||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
|
||||||
): Components => ({
|
): Components => ({
|
||||||
code: ({
|
code: ({
|
||||||
node,
|
node,
|
||||||
@ -36,92 +25,39 @@ export const createMarkdownComponents = (
|
|||||||
const match = /language-(\w+)/.exec(className || "")
|
const match = /language-(\w+)/.exec(className || "")
|
||||||
|
|
||||||
return match ? (
|
return match ? (
|
||||||
<div className="relative border border-input rounded-md mt-8 my-2 translate-y-[-1rem]">
|
<div className="relative border border-input rounded-md my-4">
|
||||||
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 rounded-tl">
|
<div className="absolute top-0 left-0 px-2 py-1 text-xs font-semibold text-gray-200 bg-#1e1e1e rounded-tl">
|
||||||
{match[1]}
|
{match[1]}
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky top-0 right-0 flex justify-end z-10">
|
<div className="absolute top-0 right-0 flex">
|
||||||
<div className="flex border border-input shadow-lg bg-background rounded-md">
|
{renderCopyButton(children)}
|
||||||
{renderCopyButton(children)}
|
<Button
|
||||||
<div className="w-px bg-input"></div>
|
onClick={(e) => {
|
||||||
{!mergeDecorationsCollection ? (
|
e.preventDefault()
|
||||||
<ApplyButton
|
e.stopPropagation()
|
||||||
code={String(children)}
|
askAboutCode(children)
|
||||||
activeFileName={activeFileName}
|
}}
|
||||||
activeFileContent={activeFileContent}
|
size="sm"
|
||||||
editorRef={editorRef}
|
variant="ghost"
|
||||||
onApply={handleApplyCode}
|
className="p-1 h-6"
|
||||||
/>
|
>
|
||||||
) : (
|
<CornerUpLeft className="w-4 h-4" />
|
||||||
<>
|
</Button>
|
||||||
<Button
|
</div>
|
||||||
onClick={() => {
|
<div className="pt-6">
|
||||||
if (
|
<SyntaxHighlighter
|
||||||
setMergeDecorationsCollection &&
|
style={vscDarkPlus as any}
|
||||||
mergeDecorationsCollection &&
|
language={match[1]}
|
||||||
editorRef?.current
|
PreTag="div"
|
||||||
) {
|
customStyle={{
|
||||||
mergeDecorationsCollection?.clear()
|
margin: 0,
|
||||||
setMergeDecorationsCollection(undefined)
|
padding: "0.5rem",
|
||||||
}
|
fontSize: "0.875rem",
|
||||||
}}
|
}}
|
||||||
size="sm"
|
>
|
||||||
variant="ghost"
|
{stringifyContent(children)}
|
||||||
className="p-1 h-6"
|
</SyntaxHighlighter>
|
||||||
title="Accept Changes"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4 text-green-500" />
|
|
||||||
</Button>
|
|
||||||
<div className="w-px bg-input"></div>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (editorRef?.current && mergeDecorationsCollection) {
|
|
||||||
const model = editorRef.current.getModel()
|
|
||||||
if (model && (model as any).originalContent) {
|
|
||||||
editorRef.current?.setValue(
|
|
||||||
(model as any).originalContent
|
|
||||||
)
|
|
||||||
mergeDecorationsCollection.clear()
|
|
||||||
setMergeDecorationsCollection?.(undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="p-1 h-6"
|
|
||||||
title="Discard Changes"
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4 text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="w-px bg-input"></div>
|
|
||||||
<Button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
askAboutCode(children)
|
|
||||||
}}
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="p-1 h-6"
|
|
||||||
>
|
|
||||||
<CornerUpLeft className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<SyntaxHighlighter
|
|
||||||
style={vscDarkPlus as any}
|
|
||||||
language={match[1]}
|
|
||||||
PreTag="div"
|
|
||||||
customStyle={{
|
|
||||||
margin: 0,
|
|
||||||
padding: "0.5rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{stringifyContent(children)}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<code className={className} {...props}>
|
<code className={className} {...props}>
|
||||||
@ -130,62 +66,8 @@ export const createMarkdownComponents = (
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
// Render markdown elements
|
// Render markdown elements
|
||||||
p: ({ node, children, ...props }) => {
|
p: ({ node, children, ...props }) =>
|
||||||
const content = stringifyContent(children)
|
renderMarkdownElement({ node, children, ...props }),
|
||||||
const { socket } = useSocket()
|
|
||||||
|
|
||||||
if (isFilePath(content)) {
|
|
||||||
const isNewFile = content.endsWith("(new file)")
|
|
||||||
const filePath = (
|
|
||||||
isNewFile ? content.replace(" (new file)", "") : content
|
|
||||||
)
|
|
||||||
.split("/")
|
|
||||||
.filter((part, index) => index !== 0)
|
|
||||||
.join("/")
|
|
||||||
|
|
||||||
const handleFileClick = () => {
|
|
||||||
if (isNewFile) {
|
|
||||||
socket?.emit(
|
|
||||||
"createFile",
|
|
||||||
{
|
|
||||||
name: filePath,
|
|
||||||
},
|
|
||||||
(response: any) => {
|
|
||||||
if (response.success) {
|
|
||||||
const tab: TTab = {
|
|
||||||
id: filePath,
|
|
||||||
name: filePath.split("/").pop() || "",
|
|
||||||
saved: true,
|
|
||||||
type: "file",
|
|
||||||
}
|
|
||||||
selectFile(tab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
const tab: TTab = {
|
|
||||||
id: filePath,
|
|
||||||
name: filePath.split("/").pop() || "",
|
|
||||||
saved: true,
|
|
||||||
type: "file",
|
|
||||||
}
|
|
||||||
selectFile(tab)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onClick={handleFileClick}
|
|
||||||
className="group flex items-center gap-2 px-2 py-1 bg-secondary/50 rounded-md my-2 text-xs hover:bg-secondary cursor-pointer w-fit"
|
|
||||||
>
|
|
||||||
<FileText className="h-4 w-4" />
|
|
||||||
<span className="font-mono group-hover:underline">{content}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderMarkdownElement({ node, children, ...props })
|
|
||||||
},
|
|
||||||
h1: ({ node, children, ...props }) =>
|
h1: ({ node, children, ...props }) =>
|
||||||
renderMarkdownElement({ node, children, ...props }),
|
renderMarkdownElement({ node, children, ...props }),
|
||||||
h2: ({ node, children, ...props }) =>
|
h2: ({ node, children, ...props }) =>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { TemplateConfig } from "@/lib/templates"
|
import { TemplateConfig } from "@/lib/templates"
|
||||||
import { TFile, TFolder, TTab } from "@/lib/types"
|
import { TFile, TFolder } from "@/lib/types"
|
||||||
import * as monaco from "monaco-editor"
|
import * as monaco from "monaco-editor"
|
||||||
import { Socket } from "socket.io-client"
|
import { Socket } from "socket.io-client"
|
||||||
|
|
||||||
@ -56,11 +56,6 @@ export interface AIChatProps {
|
|||||||
files: (TFile | TFolder)[]
|
files: (TFile | TFolder)[]
|
||||||
templateType: string
|
templateType: string
|
||||||
templateConfig?: TemplateConfig
|
templateConfig?: TemplateConfig
|
||||||
projectName: string
|
|
||||||
handleApplyCode: (mergedCode: string, originalCode: string) => void
|
|
||||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
|
|
||||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
|
||||||
selectFile: (tab: TTab) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat input props interface
|
// Chat input props interface
|
||||||
@ -110,13 +105,6 @@ export interface MessageProps {
|
|||||||
) => void
|
) => void
|
||||||
setIsContextExpanded: (isExpanded: boolean) => void
|
setIsContextExpanded: (isExpanded: boolean) => void
|
||||||
socket: Socket | null
|
socket: Socket | null
|
||||||
handleApplyCode: (mergedCode: string, originalCode: string) => void
|
|
||||||
activeFileName: string
|
|
||||||
activeFileContent: string
|
|
||||||
editorRef: any
|
|
||||||
mergeDecorationsCollection?: monaco.editor.IEditorDecorationsCollection
|
|
||||||
setMergeDecorationsCollection?: (collection: undefined) => void
|
|
||||||
selectFile: (tab: TTab) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context tabs props interface
|
// Context tabs props interface
|
||||||
|
@ -66,17 +66,15 @@ export default function GenerateInput({
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
messages: [
|
messages: [{
|
||||||
{
|
role: "user",
|
||||||
role: "user",
|
content: regenerate ? currentPrompt : input
|
||||||
content: regenerate ? currentPrompt : input,
|
}],
|
||||||
},
|
|
||||||
],
|
|
||||||
context: null,
|
context: null,
|
||||||
activeFileContent: data.code,
|
activeFileContent: data.code,
|
||||||
isEditMode: true,
|
isEditMode: true,
|
||||||
fileName: data.fileName,
|
fileName: data.fileName,
|
||||||
line: data.line,
|
line: data.line
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ export default function CodeEditor({
|
|||||||
//SocketContext functions and effects
|
//SocketContext functions and effects
|
||||||
const { socket, setUserAndSandboxId } = useSocket()
|
const { socket, setUserAndSandboxId } = useSocket()
|
||||||
// theme
|
// theme
|
||||||
const { resolvedTheme: theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Ensure userData.id and sandboxData.id are available before attempting to connect
|
// Ensure userData.id and sandboxData.id are available before attempting to connect
|
||||||
if (userData.id && sandboxData.id) {
|
if (userData.id && sandboxData.id) {
|
||||||
@ -104,13 +104,6 @@ export default function CodeEditor({
|
|||||||
// Added this state to track the most recent content for each file
|
// Added this state to track the most recent content for each file
|
||||||
const [fileContents, setFileContents] = useState<Record<string, string>>({})
|
const [fileContents, setFileContents] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
// Apply Button merger decoration state
|
|
||||||
const [mergeDecorations, setMergeDecorations] = useState<
|
|
||||||
monaco.editor.IModelDeltaDecoration[]
|
|
||||||
>([])
|
|
||||||
const [mergeDecorationsCollection, setMergeDecorationsCollection] =
|
|
||||||
useState<monaco.editor.IEditorDecorationsCollection>()
|
|
||||||
|
|
||||||
// Editor state
|
// Editor state
|
||||||
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
const [editorLanguage, setEditorLanguage] = useState("plaintext")
|
||||||
const [cursorLine, setCursorLine] = useState(0)
|
const [cursorLine, setCursorLine] = useState(0)
|
||||||
@ -382,57 +375,6 @@ export default function CodeEditor({
|
|||||||
})
|
})
|
||||||
}, [editorRef])
|
}, [editorRef])
|
||||||
|
|
||||||
// handle apply code
|
|
||||||
const handleApplyCode = useCallback(
|
|
||||||
(mergedCode: string, originalCode: string) => {
|
|
||||||
if (!editorRef) return
|
|
||||||
|
|
||||||
const model = editorRef.getModel()
|
|
||||||
if (!model) return // Store original content
|
|
||||||
;(model as any).originalContent = originalCode
|
|
||||||
|
|
||||||
// Calculate the full range of the document
|
|
||||||
const fullRange = model.getFullModelRange()
|
|
||||||
|
|
||||||
// Create decorations before applying the edit
|
|
||||||
const originalLines = originalCode.split("\n")
|
|
||||||
const mergedLines = mergedCode.split("\n")
|
|
||||||
const decorations: monaco.editor.IModelDeltaDecoration[] = []
|
|
||||||
|
|
||||||
for (
|
|
||||||
let i = 0;
|
|
||||||
i < Math.max(originalLines.length, mergedLines.length);
|
|
||||||
i++
|
|
||||||
) {
|
|
||||||
// Only highlight new lines (green highlights)
|
|
||||||
if (i >= originalLines.length || originalLines[i] !== mergedLines[i]) {
|
|
||||||
decorations.push({
|
|
||||||
range: new monaco.Range(i + 1, 1, i + 1, 1),
|
|
||||||
options: {
|
|
||||||
isWholeLine: true,
|
|
||||||
className: "added-line-decoration",
|
|
||||||
glyphMarginClassName: "added-line-glyph",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the edit operation
|
|
||||||
editorRef.executeEdits("apply-code", [
|
|
||||||
{
|
|
||||||
range: fullRange,
|
|
||||||
text: mergedCode,
|
|
||||||
forceMoveMarkers: true,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
// Apply decorations after the edit
|
|
||||||
const newDecorations = editorRef.createDecorationsCollection(decorations)
|
|
||||||
setMergeDecorationsCollection(newDecorations)
|
|
||||||
},
|
|
||||||
[editorRef]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate widget effect
|
// Generate widget effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (generate.show) {
|
if (generate.show) {
|
||||||
@ -793,32 +735,31 @@ export default function CodeEditor({
|
|||||||
setGenerate((prev) => ({ ...prev, show: false }))
|
setGenerate((prev) => ({ ...prev, show: false }))
|
||||||
|
|
||||||
// Check if the tab already exists in the list of open tabs
|
// Check if the tab already exists in the list of open tabs
|
||||||
const existingTab = tabs.find((t) => t.id === tab.id)
|
const exists = tabs.find((t) => t.id === tab.id)
|
||||||
|
setTabs((prev) => {
|
||||||
if (existingTab) {
|
if (exists) {
|
||||||
// If the tab exists, just make it active
|
// If the tab exists, make it the active tab
|
||||||
setActiveFileId(existingTab.id)
|
setActiveFileId(exists.id)
|
||||||
if (fileContents[existingTab.id]) {
|
return prev
|
||||||
setActiveFileContent(fileContents[existingTab.id])
|
|
||||||
}
|
}
|
||||||
|
// If the tab doesn't exist, add it to the list of tabs and make it active
|
||||||
|
return [...prev, tab]
|
||||||
|
})
|
||||||
|
|
||||||
|
// If the file's content is already cached, set it as the active content
|
||||||
|
if (fileContents[tab.id]) {
|
||||||
|
setActiveFileContent(fileContents[tab.id])
|
||||||
} else {
|
} else {
|
||||||
// If the tab doesn't exist, add it to the list and make it active
|
// Otherwise, fetch the content of the file and cache it
|
||||||
setTabs((prev) => [...prev, tab])
|
debouncedGetFile(tab.id, (response: string) => {
|
||||||
|
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
|
||||||
// Fetch content if not cached
|
setActiveFileContent(response)
|
||||||
if (!fileContents[tab.id]) {
|
})
|
||||||
debouncedGetFile(tab.id, (response: string) => {
|
|
||||||
setFileContents((prev) => ({ ...prev, [tab.id]: response }))
|
|
||||||
setActiveFileContent(response)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setActiveFileContent(fileContents[tab.id])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the editor language based on the file type
|
// Set the editor language based on the file type
|
||||||
setEditorLanguage(processFileType(tab.name))
|
setEditorLanguage(processFileType(tab.name))
|
||||||
// Set the active file ID
|
// Set the active file ID to the new tab
|
||||||
setActiveFileId(tab.id)
|
setActiveFileId(tab.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -980,9 +921,9 @@ export default function CodeEditor({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-full overflow-hidden">
|
<>
|
||||||
|
{/* Copilot DOM elements */}
|
||||||
<PreviewProvider>
|
<PreviewProvider>
|
||||||
{/* Copilot DOM elements */}
|
|
||||||
<div ref={generateRef} />
|
<div ref={generateRef} />
|
||||||
<div ref={suggestionRef} className="absolute">
|
<div ref={suggestionRef} className="absolute">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@ -1146,62 +1087,62 @@ export default function CodeEditor({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
) : // Note clerk.loaded is required here due to a bug: https://github.com/clerk/javascript/issues/1643
|
||||||
clerk.loaded ? (
|
clerk.loaded ? (
|
||||||
<>
|
<>
|
||||||
{/* {provider && userInfo ? (
|
{/* {provider && userInfo ? (
|
||||||
<Cursors yProvider={provider} userInfo={userInfo} />
|
<Cursors yProvider={provider} userInfo={userInfo} />
|
||||||
) : null} */}
|
) : null} */}
|
||||||
<Editor
|
<Editor
|
||||||
height="100%"
|
height="100%"
|
||||||
language={editorLanguage}
|
language={editorLanguage}
|
||||||
beforeMount={handleEditorWillMount}
|
beforeMount={handleEditorWillMount}
|
||||||
onMount={handleEditorMount}
|
onMount={handleEditorMount}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
// If the new content is different from the cached content, update it
|
// If the new content is different from the cached content, update it
|
||||||
if (value !== fileContents[activeFileId]) {
|
if (value !== fileContents[activeFileId]) {
|
||||||
setActiveFileContent(value ?? "") // Update the active file content
|
setActiveFileContent(value ?? "") // Update the active file content
|
||||||
// Mark the file as unsaved by setting 'saved' to false
|
// Mark the file as unsaved by setting 'saved' to false
|
||||||
setTabs((prev) =>
|
setTabs((prev) =>
|
||||||
prev.map((tab) =>
|
prev.map((tab) =>
|
||||||
tab.id === activeFileId
|
tab.id === activeFileId
|
||||||
? { ...tab, saved: false }
|
? { ...tab, saved: false }
|
||||||
: tab
|
: tab
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
} else {
|
||||||
} else {
|
// If the content matches the cached content, mark the file as saved
|
||||||
// If the content matches the cached content, mark the file as saved
|
setTabs((prev) =>
|
||||||
setTabs((prev) =>
|
prev.map((tab) =>
|
||||||
prev.map((tab) =>
|
tab.id === activeFileId
|
||||||
tab.id === activeFileId
|
? { ...tab, saved: true }
|
||||||
? { ...tab, saved: true }
|
: tab
|
||||||
: tab
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}}
|
||||||
}}
|
options={{
|
||||||
options={{
|
tabSize: 2,
|
||||||
tabSize: 2,
|
minimap: {
|
||||||
minimap: {
|
enabled: false,
|
||||||
enabled: false,
|
},
|
||||||
},
|
padding: {
|
||||||
padding: {
|
bottom: 4,
|
||||||
bottom: 4,
|
top: 4,
|
||||||
top: 4,
|
},
|
||||||
},
|
scrollBeyondLastLine: false,
|
||||||
scrollBeyondLastLine: false,
|
fixedOverflowWidgets: true,
|
||||||
fixedOverflowWidgets: true,
|
fontFamily: "var(--font-geist-mono)",
|
||||||
fontFamily: "var(--font-geist-mono)",
|
}}
|
||||||
}}
|
theme={theme === "light" ? "vs" : "vs-dark"}
|
||||||
theme={theme === "light" ? "vs" : "vs-dark"}
|
value={activeFileContent}
|
||||||
value={activeFileContent}
|
/>
|
||||||
/>
|
</>
|
||||||
</>
|
) : (
|
||||||
) : (
|
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
||||||
<div className="w-full h-full flex items-center justify-center text-xl font-medium text-muted-foreground/50 select-none">
|
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
||||||
<Loader2 className="animate-spin w-6 h-6 mr-3" />
|
Waiting for Clerk to load...
|
||||||
Waiting for Clerk to load...
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
@ -1293,18 +1234,13 @@ export default function CodeEditor({
|
|||||||
lastCopiedRangeRef={lastCopiedRangeRef}
|
lastCopiedRangeRef={lastCopiedRangeRef}
|
||||||
files={files}
|
files={files}
|
||||||
templateType={sandboxData.type}
|
templateType={sandboxData.type}
|
||||||
projectName={sandboxData.name}
|
|
||||||
handleApplyCode={handleApplyCode}
|
|
||||||
mergeDecorationsCollection={mergeDecorationsCollection}
|
|
||||||
setMergeDecorationsCollection={setMergeDecorationsCollection}
|
|
||||||
selectFile={selectFile}
|
|
||||||
/>
|
/>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</PreviewProvider>
|
</PreviewProvider>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
import { useTerminal } from "@/context/TerminalContext"
|
import { useTerminal } from "@/context/TerminalContext"
|
||||||
import { Sandbox, User } from "@/lib/types"
|
import { Sandbox, User } from "@/lib/types"
|
||||||
import { Globe } from "lucide-react"
|
import { Globe } from "lucide-react"
|
||||||
import { useEffect, useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
export default function DeployButtonModal({
|
export default function DeployButtonModal({
|
||||||
userData,
|
userData,
|
||||||
@ -18,21 +18,8 @@ export default function DeployButtonModal({
|
|||||||
userData: User
|
userData: User
|
||||||
data: Sandbox
|
data: Sandbox
|
||||||
}) {
|
}) {
|
||||||
const { deploy, getAppExists } = useTerminal()
|
const { deploy } = useTerminal()
|
||||||
const [isDeploying, setIsDeploying] = useState(false)
|
const [isDeploying, setIsDeploying] = useState(false)
|
||||||
const [isDeployed, setIsDeployed] = useState(false)
|
|
||||||
const [deployButtonVisible, setDeployButtonEnabled] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkDeployment = async () => {
|
|
||||||
if (getAppExists) {
|
|
||||||
const exists = await getAppExists(data.id)
|
|
||||||
setDeployButtonEnabled(exists.success)
|
|
||||||
setIsDeployed((exists.success && exists.exists) ?? false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkDeployment()
|
|
||||||
}, [data.id, getAppExists])
|
|
||||||
|
|
||||||
const handleDeploy = () => {
|
const handleDeploy = () => {
|
||||||
if (isDeploying) {
|
if (isDeploying) {
|
||||||
@ -43,7 +30,6 @@ export default function DeployButtonModal({
|
|||||||
setIsDeploying(true)
|
setIsDeploying(true)
|
||||||
deploy(() => {
|
deploy(() => {
|
||||||
setIsDeploying(false)
|
setIsDeploying(false)
|
||||||
setIsDeployed(true)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,12 +38,10 @@ export default function DeployButtonModal({
|
|||||||
<>
|
<>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
{deployButtonVisible && (
|
<Button variant="outline">
|
||||||
<Button variant="outline">
|
<Globe className="w-4 h-4 mr-2" />
|
||||||
<Globe className="w-4 h-4 mr-2" />
|
Deploy
|
||||||
Deploy
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg"
|
className="p-4 w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-lg xl:max-w-xl rounded-lg shadow-lg"
|
||||||
@ -68,9 +52,8 @@ export default function DeployButtonModal({
|
|||||||
<DeploymentOption
|
<DeploymentOption
|
||||||
icon={<Globe className="text-gray-500 w-5 h-5" />}
|
icon={<Globe className="text-gray-500 w-5 h-5" />}
|
||||||
domain={`${data.id}.gitwit.app`}
|
domain={`${data.id}.gitwit.app`}
|
||||||
timestamp="Deployed 1m ago"
|
timestamp="Deployed 1h ago"
|
||||||
user={userData.name}
|
user={userData.name}
|
||||||
isDeployed={isDeployed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -78,7 +61,7 @@ export default function DeployButtonModal({
|
|||||||
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
|
className="mt-4 w-full bg-[#0a0a0a] text-white hover:bg-[#262626]"
|
||||||
onClick={handleDeploy}
|
onClick={handleDeploy}
|
||||||
>
|
>
|
||||||
{isDeploying ? "Deploying..." : isDeployed ? "Update" : "Deploy"}
|
{isDeploying ? "Deploying..." : "Update"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
@ -91,33 +74,27 @@ function DeploymentOption({
|
|||||||
domain,
|
domain,
|
||||||
timestamp,
|
timestamp,
|
||||||
user,
|
user,
|
||||||
isDeployed,
|
|
||||||
}: {
|
}: {
|
||||||
icon: React.ReactNode
|
icon: React.ReactNode
|
||||||
domain: string
|
domain: string
|
||||||
timestamp: string
|
timestamp: string
|
||||||
user: string
|
user: string
|
||||||
isDeployed: boolean
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900">
|
<div className="flex flex-col gap-2 w-full text-left p-2 rounded-md border border-gray-700 bg-gray-900">
|
||||||
<div className="flex items-start gap-2 relative">
|
<div className="flex items-start gap-2 relative">
|
||||||
<div className="flex-shrink-0">{icon}</div>
|
<div className="flex-shrink-0">{icon}</div>
|
||||||
{isDeployed ? (
|
<a
|
||||||
<a
|
href={`https://${domain}`}
|
||||||
href={`https://${domain}`}
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
className="font-semibold text-gray-300 hover:underline"
|
||||||
className="font-semibold text-gray-300 hover:underline"
|
>
|
||||||
>
|
{domain}
|
||||||
{domain}
|
</a>
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<span className="font-semibold text-gray-300">{domain}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-400 mt-0 ml-7">
|
<p className="text-sm text-gray-400 mt-0 ml-7">
|
||||||
{isDeployed ? `${timestamp} • ${user}` : "Never deployed"}
|
{timestamp} • {user}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -35,7 +35,7 @@ export default function RunButtonModal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [terminals, isRunning])
|
}, [terminals, isRunning])
|
||||||
// commands to run in the terminal
|
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
if (isRunning && lastCreatedTerminalRef.current) {
|
if (isRunning && lastCreatedTerminalRef.current) {
|
||||||
await closeTerminal(lastCreatedTerminalRef.current)
|
await closeTerminal(lastCreatedTerminalRef.current)
|
||||||
|
@ -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"
|
||||||
|
@ -7,7 +7,7 @@ import "./xterm.css"
|
|||||||
import { debounce } from "@/lib/utils"
|
import { debounce } from "@/lib/utils"
|
||||||
import { Loader2 } from "lucide-react"
|
import { Loader2 } from "lucide-react"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { ElementRef, useEffect, useRef, useCallback } from "react"
|
import { ElementRef, useEffect, useRef } from "react"
|
||||||
import { Socket } from "socket.io-client"
|
import { Socket } from "socket.io-client"
|
||||||
export default function EditorTerminal({
|
export default function EditorTerminal({
|
||||||
socket,
|
socket,
|
||||||
@ -22,12 +22,13 @@ export default function EditorTerminal({
|
|||||||
setTerm: (term: Terminal) => void
|
setTerm: (term: Terminal) => void
|
||||||
visible: boolean
|
visible: boolean
|
||||||
}) {
|
}) {
|
||||||
const { resolvedTheme: theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const terminalContainerRef = useRef<ElementRef<"div">>(null)
|
const terminalContainerRef = useRef<ElementRef<"div">>(null)
|
||||||
const fitAddonRef = useRef<FitAddon | null>(null)
|
const fitAddonRef = useRef<FitAddon | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!terminalContainerRef.current) return
|
if (!terminalContainerRef.current) return
|
||||||
|
// console.log("new terminal", id, term ? "reusing" : "creating");
|
||||||
|
|
||||||
const terminal = new Terminal({
|
const terminal = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
@ -36,54 +37,13 @@ export default function EditorTerminal({
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
allowTransparency: true,
|
|
||||||
rightClickSelectsWord: true,
|
|
||||||
allowProposedApi: true, // for custom key events
|
|
||||||
})
|
|
||||||
|
|
||||||
// right-click paste handler
|
|
||||||
const handleContextMenu = (e: MouseEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
navigator.clipboard.readText().then((text) => {
|
|
||||||
if (text) {
|
|
||||||
socket.emit("terminalData", { id, data: text })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
terminalContainerRef.current.addEventListener(
|
|
||||||
"contextmenu",
|
|
||||||
handleContextMenu
|
|
||||||
)
|
|
||||||
|
|
||||||
// keyboard paste handler
|
|
||||||
terminal.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
|
||||||
if (event.type === "keydown") {
|
|
||||||
if (
|
|
||||||
(event.ctrlKey || event.metaKey) &&
|
|
||||||
event.key.toLowerCase() === "v"
|
|
||||||
) {
|
|
||||||
event.preventDefault()
|
|
||||||
navigator.clipboard.readText().then((text) => {
|
|
||||||
if (text) {
|
|
||||||
socket.emit("terminalData", { id, data: text })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setTerm(terminal)
|
setTerm(terminal)
|
||||||
|
const dispose = () => {
|
||||||
return () => {
|
|
||||||
terminal.dispose()
|
terminal.dispose()
|
||||||
terminalContainerRef.current?.removeEventListener(
|
|
||||||
"contextmenu",
|
|
||||||
handleContextMenu
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return dispose
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -121,6 +81,7 @@ export default function EditorTerminal({
|
|||||||
|
|
||||||
const { width, height } = entry.contentRect
|
const { width, height } = entry.contentRect
|
||||||
|
|
||||||
|
// Only call fit if the size has actually changed
|
||||||
if (
|
if (
|
||||||
width !== terminalContainerRef.current.offsetWidth ||
|
width !== terminalContainerRef.current.offsetWidth ||
|
||||||
height !== terminalContainerRef.current.offsetHeight
|
height !== terminalContainerRef.current.offsetHeight
|
||||||
@ -131,9 +92,10 @@ export default function EditorTerminal({
|
|||||||
console.error("Error during fit:", err)
|
console.error("Error during fit:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 50)
|
}, 50) // Debounce for 50ms
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// start observing for resize
|
||||||
resizeObserver.observe(terminalContainerRef.current)
|
resizeObserver.observe(terminalContainerRef.current)
|
||||||
return () => {
|
return () => {
|
||||||
disposableOnData.dispose()
|
disposableOnData.dispose()
|
||||||
@ -162,7 +124,6 @@ export default function EditorTerminal({
|
|||||||
ref={terminalContainerRef}
|
ref={terminalContainerRef}
|
||||||
style={{ display: visible ? "block" : "none" }}
|
style={{ display: visible ? "block" : "none" }}
|
||||||
className="w-full h-full text-left"
|
className="w-full h-full text-left"
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
{term === null ? (
|
{term === null ? (
|
||||||
<div className="flex items-center text-muted-foreground p-2">
|
<div className="flex items-center text-muted-foreground p-2">
|
||||||
|
@ -1,749 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import NewProjectModal from "@/components/dashboard/newProject"
|
|
||||||
import ProjectCard from "@/components/dashboard/projectCard/"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card"
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form"
|
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from "@/components/ui/hover-card"
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip"
|
|
||||||
import { deleteSandbox, updateSandbox, updateUser } from "@/lib/actions"
|
|
||||||
import { socialIcons } from "@/lib/data"
|
|
||||||
import { editUserSchema, EditUserSchema } from "@/lib/schema"
|
|
||||||
import { TIERS } from "@/lib/tiers"
|
|
||||||
import { SandboxWithLiked, User, UserLink } from "@/lib/types"
|
|
||||||
import { cn, parseSocialLink } from "@/lib/utils"
|
|
||||||
import { useUser } from "@clerk/nextjs"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
|
||||||
import {
|
|
||||||
Edit,
|
|
||||||
Globe,
|
|
||||||
Heart,
|
|
||||||
Info,
|
|
||||||
Loader2,
|
|
||||||
LucideIcon,
|
|
||||||
Package2,
|
|
||||||
PlusCircle,
|
|
||||||
Sparkles,
|
|
||||||
Trash2,
|
|
||||||
X,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import {
|
|
||||||
Fragment,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
useTransition,
|
|
||||||
} from "react"
|
|
||||||
import { useFormState, useFormStatus } from "react-dom"
|
|
||||||
import { useFieldArray, useForm } from "react-hook-form"
|
|
||||||
import { toast } from "sonner"
|
|
||||||
import Avatar from "../ui/avatar"
|
|
||||||
import { Badge } from "../ui/badge"
|
|
||||||
import { Input } from "../ui/input"
|
|
||||||
import { Progress } from "../ui/progress"
|
|
||||||
import { Textarea } from "../ui/textarea"
|
|
||||||
// #region Profile Page
|
|
||||||
export default function ProfilePage({
|
|
||||||
publicSandboxes,
|
|
||||||
privateSandboxes,
|
|
||||||
profileOwner,
|
|
||||||
loggedInUser,
|
|
||||||
}: {
|
|
||||||
publicSandboxes: SandboxWithLiked[]
|
|
||||||
privateSandboxes: SandboxWithLiked[]
|
|
||||||
profileOwner: User
|
|
||||||
loggedInUser: User | null
|
|
||||||
}) {
|
|
||||||
const isOwnProfile = profileOwner.id === loggedInUser?.id
|
|
||||||
|
|
||||||
const sandboxes = useMemo(() => {
|
|
||||||
const allSandboxes = isOwnProfile
|
|
||||||
? [...publicSandboxes, ...privateSandboxes]
|
|
||||||
: publicSandboxes
|
|
||||||
|
|
||||||
return allSandboxes
|
|
||||||
}, [isOwnProfile, publicSandboxes, privateSandboxes])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="container mx-auto p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div className="md:col-span-1">
|
|
||||||
<ProfileCard
|
|
||||||
name={profileOwner.name}
|
|
||||||
username={profileOwner.username}
|
|
||||||
avatarUrl={profileOwner.avatarUrl}
|
|
||||||
sandboxes={sandboxes}
|
|
||||||
joinedDate={profileOwner.createdAt}
|
|
||||||
generations={isOwnProfile ? loggedInUser.generations : undefined}
|
|
||||||
isOwnProfile={isOwnProfile}
|
|
||||||
tier={profileOwner.tier}
|
|
||||||
bio={profileOwner.bio}
|
|
||||||
personalWebsite={profileOwner.personalWebsite}
|
|
||||||
socialLinks={profileOwner.links}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<SandboxesPanel
|
|
||||||
{...{
|
|
||||||
publicSandboxes,
|
|
||||||
privateSandboxes,
|
|
||||||
isOwnProfile,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region Profile Card
|
|
||||||
function ProfileCard({
|
|
||||||
name,
|
|
||||||
username,
|
|
||||||
avatarUrl,
|
|
||||||
sandboxes,
|
|
||||||
joinedDate,
|
|
||||||
generations,
|
|
||||||
isOwnProfile,
|
|
||||||
bio,
|
|
||||||
personalWebsite,
|
|
||||||
socialLinks = [],
|
|
||||||
tier,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
username: string
|
|
||||||
avatarUrl: string | null
|
|
||||||
bio: string | null
|
|
||||||
personalWebsite: string | null
|
|
||||||
socialLinks: UserLink[]
|
|
||||||
sandboxes: SandboxWithLiked[]
|
|
||||||
joinedDate: Date
|
|
||||||
generations?: number
|
|
||||||
isOwnProfile: boolean
|
|
||||||
tier: string
|
|
||||||
}) {
|
|
||||||
const { user } = useUser()
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
|
|
||||||
const joinedAt = useMemo(() => {
|
|
||||||
const date = new Date(joinedDate).toLocaleDateString("en-US", {
|
|
||||||
month: "long",
|
|
||||||
year: "numeric",
|
|
||||||
})
|
|
||||||
return `Joined ${date}`
|
|
||||||
}, [joinedDate])
|
|
||||||
const toggleEdit = useCallback(() => {
|
|
||||||
setIsEditing((s) => !s)
|
|
||||||
}, [])
|
|
||||||
const stats = useMemo(() => {
|
|
||||||
const totalSandboxes = sandboxes.length
|
|
||||||
const totalLikes = sandboxes.reduce(
|
|
||||||
(sum, sandbox) => sum + sandbox.likeCount,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
sandboxes:
|
|
||||||
totalSandboxes === 1 ? "1 sandbox" : `${totalSandboxes} sandboxes`,
|
|
||||||
likes: totalLikes === 1 ? "1 like" : `${totalLikes} likes`,
|
|
||||||
}
|
|
||||||
}, [sandboxes])
|
|
||||||
|
|
||||||
const showAddMoreInfoBanner = useMemo(() => {
|
|
||||||
return !bio && !personalWebsite && (socialLinks?.length ?? 0) === 0
|
|
||||||
}, [personalWebsite, bio, socialLinks])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="mb-6 md:mb-0 sticky top-6">
|
|
||||||
{isOwnProfile && (
|
|
||||||
<div className="absolute top-2 right-2 flex flex-col gap-2">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
onClick={toggleEdit}
|
|
||||||
aria-label={isEditing ? "close edit form" : "open edit form"}
|
|
||||||
size="smIcon"
|
|
||||||
variant="secondary"
|
|
||||||
className="rounded-full relative"
|
|
||||||
>
|
|
||||||
{isEditing ? (
|
|
||||||
<X className="size-4" />
|
|
||||||
) : showAddMoreInfoBanner ? (
|
|
||||||
<>
|
|
||||||
<Sparkles className="size-4 text-yellow-400 z-[2]" />
|
|
||||||
<div className="z-[1] absolute inset-0 rounded-full bg-secondary animate-ping" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Edit className="size-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
{showAddMoreInfoBanner
|
|
||||||
? "Add more information to your profile"
|
|
||||||
: "Edit your profile"}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<CardContent className="flex flex-col gap-4 pt-6">
|
|
||||||
{isEditing ? (
|
|
||||||
<div className="flex flex-col gap-2 items-center ">
|
|
||||||
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
|
|
||||||
<EditProfileForm
|
|
||||||
{...{
|
|
||||||
name,
|
|
||||||
username,
|
|
||||||
avatarUrl,
|
|
||||||
bio,
|
|
||||||
personalWebsite,
|
|
||||||
socialLinks,
|
|
||||||
toggleEdit,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-2 items-center">
|
|
||||||
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
|
|
||||||
<div className="space-y-1">
|
|
||||||
<CardTitle className="text-2xl text-center">{name}</CardTitle>
|
|
||||||
<CardDescription className="text-center">{`@${username}`}</CardDescription>
|
|
||||||
</div>
|
|
||||||
{bio && <p className="text-sm text-center">{bio}</p>}
|
|
||||||
{(socialLinks.length > 0 || personalWebsite) && (
|
|
||||||
<div className="flex gap-2 justify-center">
|
|
||||||
{personalWebsite && (
|
|
||||||
<Button variant="secondary" size="smIcon" asChild>
|
|
||||||
<a
|
|
||||||
href={personalWebsite}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Globe className="size-4" />
|
|
||||||
<span className="sr-only">Personal Website</span>
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{socialLinks.map((link, index) => {
|
|
||||||
const Icon = socialIcons[link.platform]
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={index}
|
|
||||||
variant="secondary"
|
|
||||||
size="smIcon"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={link.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Icon className="size-4" />
|
|
||||||
<span className="sr-only">{link.platform}</span>
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1 items-center">
|
|
||||||
{typeof generations === "number" && (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<SubscriptionBadge
|
|
||||||
generations={generations}
|
|
||||||
tier={tier as keyof typeof TIERS}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<StatsItem icon={Package2} label={stats.sandboxes} />
|
|
||||||
<StatsItem icon={Heart} label={stats.likes} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs mt-2 text-muted-foreground text-center">
|
|
||||||
{joinedAt}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditProfileForm(props: {
|
|
||||||
name: string
|
|
||||||
username: string
|
|
||||||
avatarUrl: string | null
|
|
||||||
bio: string | null
|
|
||||||
personalWebsite: string | null
|
|
||||||
socialLinks: UserLink[]
|
|
||||||
toggleEdit: () => void
|
|
||||||
}) {
|
|
||||||
const router = useRouter()
|
|
||||||
const { user } = useUser()
|
|
||||||
const formRef = useRef<HTMLFormElement>(null)
|
|
||||||
const [formState, formAction] = useFormState(updateUser, {
|
|
||||||
message: "",
|
|
||||||
})
|
|
||||||
const [isPending, startTransition] = useTransition()
|
|
||||||
const { name, username, bio, personalWebsite, socialLinks, toggleEdit } =
|
|
||||||
props
|
|
||||||
const form = useForm<EditUserSchema>({
|
|
||||||
resolver: zodResolver(editUserSchema),
|
|
||||||
defaultValues: {
|
|
||||||
oldUsername: username,
|
|
||||||
id: user?.id,
|
|
||||||
name,
|
|
||||||
username,
|
|
||||||
bio: bio ?? "",
|
|
||||||
personalWebsite: personalWebsite ?? "",
|
|
||||||
links:
|
|
||||||
socialLinks.length > 0
|
|
||||||
? socialLinks
|
|
||||||
: [{ url: "", platform: "generic" }],
|
|
||||||
...(formState.fields ?? {}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
|
||||||
name: "links",
|
|
||||||
control: form.control,
|
|
||||||
})
|
|
||||||
useEffect(() => {
|
|
||||||
const message = formState.message
|
|
||||||
if (!Boolean(message)) return
|
|
||||||
if ("error" in formState) {
|
|
||||||
toast.error(formState.message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toast.success(formState.message as String)
|
|
||||||
toggleEdit()
|
|
||||||
if (formState?.newRoute) {
|
|
||||||
router.replace(formState.newRoute)
|
|
||||||
}
|
|
||||||
}, [formState])
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
ref={formRef}
|
|
||||||
action={formAction}
|
|
||||||
onSubmit={(evt) => {
|
|
||||||
evt.preventDefault()
|
|
||||||
form.handleSubmit(() => {
|
|
||||||
startTransition(() => {
|
|
||||||
formAction(new FormData(formRef.current!))
|
|
||||||
})
|
|
||||||
})(evt)
|
|
||||||
}}
|
|
||||||
className="space-y-3 w-full"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="id" value={user?.id} />
|
|
||||||
<input type="hidden" name="oldUsername" value={username} />
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="marie doe" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>User name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
className="peer ps-6"
|
|
||||||
type="text"
|
|
||||||
placeholder="Username"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
<span className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-2 text-sm text-muted-foreground peer-disabled:opacity-50">
|
|
||||||
@
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="bio"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Bio</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
placeholder="hi, I love building things!"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="personalWebsite"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Personal Website</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="https://chillguy.dev" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{fields.map((field, index) => (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
key={field.id}
|
|
||||||
name={`links.${index}`}
|
|
||||||
render={({ field: { onChange, value, ...field } }) => {
|
|
||||||
const Icon = socialIcons[value.platform] ?? socialIcons.generic
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className={cn(index !== 0 && "sr-only")}>
|
|
||||||
Social Links
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription className={cn(index !== 0 && "sr-only")}>
|
|
||||||
Add links to your blogs or social media profiles.
|
|
||||||
</FormDescription>
|
|
||||||
<FormControl>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
className="peer ps-9"
|
|
||||||
value={value.url}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange(parseSocialLink(e.currentTarget.value))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 text-muted-foreground/80 peer-disabled:opacity-50">
|
|
||||||
<Icon
|
|
||||||
size={16}
|
|
||||||
strokeWidth={2}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="smIcon"
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="mt-2"
|
|
||||||
onClick={() => append({ url: "", platform: "generic" })}
|
|
||||||
>
|
|
||||||
Add URL
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<SubmitButton {...{ isPending }} />
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
function SubmitButton({ isPending }: { isPending: boolean }) {
|
|
||||||
const formStatus = useFormStatus()
|
|
||||||
const { pending } = formStatus
|
|
||||||
const pend = pending || isPending
|
|
||||||
return (
|
|
||||||
<Button size="sm" type="submit" className="w-full mt-2" disabled={pend}>
|
|
||||||
{pend && <Loader2 className="animate-spin mr-2 h-4 w-4" />}
|
|
||||||
Save Changes
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region Sandboxes Panel
|
|
||||||
function SandboxesPanel({
|
|
||||||
publicSandboxes,
|
|
||||||
privateSandboxes,
|
|
||||||
isOwnProfile,
|
|
||||||
}: {
|
|
||||||
publicSandboxes: SandboxWithLiked[]
|
|
||||||
privateSandboxes: SandboxWithLiked[]
|
|
||||||
isOwnProfile: boolean
|
|
||||||
}) {
|
|
||||||
const [deletingId, setDeletingId] = useState<string>("")
|
|
||||||
const hasPublicSandboxes = publicSandboxes.length > 0
|
|
||||||
const hasPrivateSandboxes = privateSandboxes.length > 0
|
|
||||||
|
|
||||||
const onVisibilityChange = useMemo(
|
|
||||||
() =>
|
|
||||||
async (sandbox: Pick<SandboxWithLiked, "id" | "name" | "visibility">) => {
|
|
||||||
const newVisibility =
|
|
||||||
sandbox.visibility === "public" ? "private" : "public"
|
|
||||||
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
|
|
||||||
await updateSandbox({
|
|
||||||
id: sandbox.id,
|
|
||||||
visibility: newVisibility,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const onDelete = useMemo(
|
|
||||||
() => async (sandbox: Pick<SandboxWithLiked, "id" | "name">) => {
|
|
||||||
setDeletingId(sandbox.id)
|
|
||||||
toast(`Project ${sandbox.name} deleted.`)
|
|
||||||
await deleteSandbox(sandbox.id)
|
|
||||||
setDeletingId("")
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
if (!isOwnProfile) {
|
|
||||||
return (
|
|
||||||
<div className="">
|
|
||||||
{hasPublicSandboxes ? (
|
|
||||||
<>
|
|
||||||
<h2 className="font-semibold text-xl mb-4">Sandboxes</h2>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
{publicSandboxes.map((sandbox) => {
|
|
||||||
return (
|
|
||||||
<Fragment key={sandbox.id}>
|
|
||||||
{isOwnProfile ? (
|
|
||||||
<ProjectCard
|
|
||||||
onVisibilityChange={onVisibilityChange}
|
|
||||||
onDelete={onDelete}
|
|
||||||
deletingId={deletingId}
|
|
||||||
isAuthenticated
|
|
||||||
{...sandbox}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ProjectCard isAuthenticated={false} {...sandbox} />
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<EmptyState type="private" isOwnProfile={isOwnProfile} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Tabs defaultValue="public">
|
|
||||||
<TabsList className="mb-4">
|
|
||||||
<TabsTrigger value="public">Public</TabsTrigger>
|
|
||||||
<TabsTrigger value="private">Private</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="public">
|
|
||||||
{hasPublicSandboxes ? (
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
{publicSandboxes.map((sandbox) => {
|
|
||||||
return (
|
|
||||||
<Fragment key={sandbox.id}>
|
|
||||||
{isOwnProfile ? (
|
|
||||||
<ProjectCard
|
|
||||||
onVisibilityChange={onVisibilityChange}
|
|
||||||
onDelete={onDelete}
|
|
||||||
deletingId={deletingId}
|
|
||||||
isAuthenticated
|
|
||||||
{...sandbox}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ProjectCard isAuthenticated={false} {...sandbox} />
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<EmptyState type="public" isOwnProfile={isOwnProfile} />
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="private">
|
|
||||||
{hasPrivateSandboxes ? (
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
{privateSandboxes.map((sandbox) => (
|
|
||||||
<ProjectCard
|
|
||||||
key={sandbox.id}
|
|
||||||
onVisibilityChange={onVisibilityChange}
|
|
||||||
onDelete={onDelete}
|
|
||||||
deletingId={deletingId}
|
|
||||||
isAuthenticated
|
|
||||||
{...sandbox}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<EmptyState type="private" isOwnProfile={isOwnProfile} />
|
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region Empty State
|
|
||||||
function EmptyState({
|
|
||||||
type,
|
|
||||||
isOwnProfile,
|
|
||||||
}: {
|
|
||||||
type: "public" | "private"
|
|
||||||
isOwnProfile: boolean
|
|
||||||
}) {
|
|
||||||
const [newProjectModalOpen, setNewProjectModalOpen] = useState(false)
|
|
||||||
|
|
||||||
const text = useMemo(() => {
|
|
||||||
let title: string
|
|
||||||
let description: string
|
|
||||||
switch (type) {
|
|
||||||
case "public":
|
|
||||||
title = "No public sandboxes yet"
|
|
||||||
description = isOwnProfile
|
|
||||||
? "Create your first public sandbox to share your work with the world!"
|
|
||||||
: "user has no public sandboxes"
|
|
||||||
|
|
||||||
case "private":
|
|
||||||
title = "No private sandboxes yet"
|
|
||||||
description = isOwnProfile
|
|
||||||
? "Create your first private sandbox to start working on your personal projects!"
|
|
||||||
: "user has no private sandboxes"
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}
|
|
||||||
}, [type, isOwnProfile])
|
|
||||||
const openModal = useCallback(() => setNewProjectModalOpen(true), [])
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card className="flex flex-col items-center justify-center p-6 text-center h-[300px]">
|
|
||||||
<PlusCircle className="h-12 w-12 text-muted-foreground mb-4" />
|
|
||||||
<CardTitle className="text-xl mb-2">{text.title}</CardTitle>
|
|
||||||
<CardDescription className="mb-4">{text.description}</CardDescription>
|
|
||||||
{isOwnProfile && (
|
|
||||||
<Button onClick={openModal}>
|
|
||||||
<PlusCircle className="h-4 w-4 mr-2" />
|
|
||||||
Create Sandbox
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
<NewProjectModal
|
|
||||||
open={newProjectModalOpen}
|
|
||||||
setOpen={setNewProjectModalOpen}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region StatsItem
|
|
||||||
interface StatsItemProps {
|
|
||||||
icon: LucideIcon
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const StatsItem = ({ icon: Icon, label }: StatsItemProps) => (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Icon size={16} />
|
|
||||||
<span className="text-sm text-muted-foreground">{label}</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region Sub Badge
|
|
||||||
const SubscriptionBadge = ({
|
|
||||||
generations,
|
|
||||||
tier = "FREE",
|
|
||||||
}: {
|
|
||||||
generations: number
|
|
||||||
tier?: keyof typeof TIERS
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<Badge variant="secondary" className="text-sm cursor-pointer">
|
|
||||||
{tier}
|
|
||||||
</Badge>
|
|
||||||
<HoverCard>
|
|
||||||
<HoverCardTrigger>
|
|
||||||
<Button variant="ghost" size="smIcon" className="size-[26px]">
|
|
||||||
<Info size={16} />
|
|
||||||
</Button>
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<HoverCardContent>
|
|
||||||
<div className="w-full space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="font-medium">AI Generations</span>
|
|
||||||
<span>{`${generations} / ${TIERS[tier].generations}`}</span>
|
|
||||||
</div>
|
|
||||||
<Progress
|
|
||||||
value={generations}
|
|
||||||
max={TIERS[tier].generations}
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button size="sm" className="w-full mt-4">
|
|
||||||
<Sparkles className="mr-2 h-4 w-4" /> Upgrade to Pro
|
|
||||||
</Button>
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// #endregion
|
|
@ -1,38 +0,0 @@
|
|||||||
import Logo from "@/assets/logo.svg"
|
|
||||||
import { ThemeSwitcher } from "@/components/ui/theme-switcher"
|
|
||||||
import UserButton from "@/components/ui/userButton"
|
|
||||||
import { User } from "@/lib/types"
|
|
||||||
import Image from "next/image"
|
|
||||||
import Link from "next/link"
|
|
||||||
import { Button } from "../ui/button"
|
|
||||||
|
|
||||||
export default function ProfileNavbar({ userData }: { userData: User }) {
|
|
||||||
return (
|
|
||||||
<nav className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
|
|
||||||
>
|
|
||||||
<Image src={Logo} alt="Logo" width={36} height={36} />
|
|
||||||
</Link>
|
|
||||||
<h1 className="text-xl">
|
|
||||||
<span className="font-semibold">Sandbox</span>{" "}
|
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
|
||||||
by gitwit
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<ThemeSwitcher />
|
|
||||||
{Boolean(userData?.id) ? (
|
|
||||||
<UserButton userData={userData} />
|
|
||||||
) : (
|
|
||||||
<Link href="/sign-in">
|
|
||||||
<Button>Login</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import Image from "next/image"
|
||||||
|
|
||||||
export default function Avatar({
|
export default function Avatar({
|
||||||
name,
|
name,
|
||||||
@ -21,12 +22,12 @@ export default function Avatar({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium",
|
className,
|
||||||
className
|
"w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<img
|
<Image
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt={name || "User"}
|
alt={name || "User"}
|
||||||
width={20}
|
width={20}
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
|
||||||
outline: "text-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface BadgeProps
|
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
|
||||||
VariantProps<typeof badgeVariants> {}
|
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
||||||
return (
|
|
||||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
|
@ -5,7 +5,7 @@ import * as React from "react"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition active:scale-[0.99] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@ -26,7 +26,7 @@ const buttonVariants = cva(
|
|||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: "h-10 rounded-md px-8",
|
||||||
icon: "h-9 w-9",
|
icon: "h-9 w-9",
|
||||||
smIcon: "size-8",
|
smIcon: "h-8 w-8",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const HoverCard = HoverCardPrimitive.Root
|
|
||||||
|
|
||||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
|
||||||
|
|
||||||
const HoverCardContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
|
||||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
|
||||||
<HoverCardPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
align={align}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
@ -18,7 +18,7 @@ const Progress = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
className="h-full w-full flex-1 bg-primary transition-all rounded-full"
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
/>
|
/>
|
||||||
</ProgressPrimitive.Root>
|
</ProgressPrimitive.Root>
|
||||||
|
@ -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 }
|
|
@ -1,55 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@ -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 }
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export function ThemeSwitcher() {
|
|||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="icon" className="text-foreground">
|
<Button variant="outline" size="icon" className="text-muted-foreground">
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||||
<span className="sr-only">Toggle theme</span>
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
@ -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 }
|
||||||
|
@ -9,15 +9,7 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { User } from "@/lib/types"
|
import { User } from "@/lib/types"
|
||||||
import { useClerk } from "@clerk/nextjs"
|
import { useClerk } from "@clerk/nextjs"
|
||||||
import {
|
import { Crown, LogOut, Sparkles } from "lucide-react"
|
||||||
Crown,
|
|
||||||
LayoutDashboard,
|
|
||||||
LogOut,
|
|
||||||
Sparkles,
|
|
||||||
User as UserIcon,
|
|
||||||
} from "lucide-react"
|
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import Avatar from "./avatar"
|
import Avatar from "./avatar"
|
||||||
@ -43,11 +35,7 @@ const TIER_INFO = {
|
|||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export default function UserButton({
|
export default function UserButton({ userData: initialUserData }: { userData: User }) {
|
||||||
userData: initialUserData,
|
|
||||||
}: {
|
|
||||||
userData: User
|
|
||||||
}) {
|
|
||||||
const [userData, setUserData] = useState<User>(initialUserData)
|
const [userData, setUserData] = useState<User>(initialUserData)
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const { signOut } = useClerk()
|
const { signOut } = useClerk()
|
||||||
@ -61,7 +49,7 @@ export default function UserButton({
|
|||||||
headers: {
|
headers: {
|
||||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||||
},
|
},
|
||||||
cache: "no-store",
|
cache: 'no-store'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@ -79,15 +67,12 @@ export default function UserButton({
|
|||||||
}
|
}
|
||||||
}, [isOpen])
|
}, [isOpen])
|
||||||
|
|
||||||
const tierInfo =
|
const tierInfo = TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
|
||||||
TIER_INFO[userData.tier as keyof typeof TIER_INFO] || TIER_INFO.FREE
|
|
||||||
const TierIcon = tierInfo.icon
|
const TierIcon = tierInfo.icon
|
||||||
const usagePercentage = Math.floor(
|
const usagePercentage = Math.floor((userData.generations || 0) * 100 / tierInfo.limit)
|
||||||
((userData.generations || 0) * 100) / tierInfo.limit
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleUpgrade = async () => {
|
const handleUpgrade = async () => {
|
||||||
router.push(`/@${userData.username}`)
|
router.push('/upgrade')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -105,30 +90,13 @@ export default function UserButton({
|
|||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
|
||||||
<Link href={"/dashboard"}>
|
|
||||||
<LayoutDashboard className="mr-2 size-4" />
|
|
||||||
<span>Dashboard</span>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
|
||||||
<Link href={`/@${userData.username}`}>
|
|
||||||
<UserIcon className="mr-2 size-4" />
|
|
||||||
<span>Profile</span>
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
|
|
||||||
<div className="py-1.5 px-2 w-full">
|
<div className="py-1.5 px-2 w-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<TierIcon className={`h-4 w-4 ${tierInfo.color}`} />
|
<TierIcon className={`h-4 w-4 ${tierInfo.color}`} />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">{userData.tier || "FREE"} Plan</span>
|
||||||
{userData.tier || "FREE"} Plan
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{(userData.tier === "FREE" || userData.tier === "PRO") && (
|
{/* {(userData.tier === "FREE" || userData.tier === "PRO") && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -137,50 +105,47 @@ export default function UserButton({
|
|||||||
>
|
>
|
||||||
Upgrade
|
Upgrade
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="px-2 py-1.5">
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
|
||||||
<span>AI Usage</span>
|
|
||||||
<span>
|
|
||||||
{userData.generations}/{tierInfo.limit}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-full w-full h-2 overflow-hidden bg-secondary mb-1">
|
<div className="py-1.5 px-2 w-full">
|
||||||
<div
|
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||||||
className={`h-full rounded-full transition-all duration-300 ${
|
<span>AI Usage</span>
|
||||||
usagePercentage > 90
|
<span>{userData.generations}/{tierInfo.limit}</span>
|
||||||
? "bg-red-500"
|
</div>
|
||||||
: usagePercentage > 75
|
|
||||||
? "bg-yellow-500"
|
<div className="rounded-full w-full h-2 overflow-hidden bg-secondary">
|
||||||
: tierInfo.color.replace("text-", "bg-")
|
<div
|
||||||
}`}
|
className={`h-full rounded-full transition-all duration-300 ${
|
||||||
style={{
|
usagePercentage > 90 ? 'bg-red-500' :
|
||||||
width: `${Math.min(usagePercentage, 100)}%`,
|
usagePercentage > 75 ? 'bg-yellow-500' :
|
||||||
}}
|
tierInfo.color.replace('text-', 'bg-')
|
||||||
/>
|
}`}
|
||||||
</div>
|
style={{
|
||||||
|
width: `${Math.min(usagePercentage, 100)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
{/* <DropdownMenuItem className="cursor-pointer">
|
{/* <DropdownMenuItem className="cursor-pointer">
|
||||||
<Pencil className="mr-2 size-4" />
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
<span>Edit Profile</span>
|
<span>Edit Profile</span>
|
||||||
</DropdownMenuItem> */}
|
</DropdownMenuItem> */}
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => signOut(() => router.push("/"))}
|
onClick={() => signOut(() => router.push("/"))}
|
||||||
className="!text-destructive cursor-pointer"
|
className="!text-destructive cursor-pointer"
|
||||||
>
|
>
|
||||||
<LogOut className="mr-2 size-4" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
<span>Log Out</span>
|
<span>Log Out</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,9 +20,6 @@ interface TerminalContextType {
|
|||||||
createNewTerminal: (command?: string) => Promise<void>
|
createNewTerminal: (command?: string) => Promise<void>
|
||||||
closeTerminal: (id: string) => void
|
closeTerminal: (id: string) => void
|
||||||
deploy: (callback: () => void) => void
|
deploy: (callback: () => void) => void
|
||||||
getAppExists:
|
|
||||||
| ((appName: string) => Promise<{ success: boolean; exists?: boolean }>)
|
|
||||||
| null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TerminalContext = createContext<TerminalContextType | undefined>(
|
const TerminalContext = createContext<TerminalContextType | undefined>(
|
||||||
@ -38,19 +35,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
>([])
|
>([])
|
||||||
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
const [activeTerminalId, setActiveTerminalId] = useState<string>("")
|
||||||
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false)
|
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false)
|
||||||
const [isSocketReady, setIsSocketReady] = useState<boolean>(false)
|
|
||||||
|
|
||||||
// Listen for the "ready" signal from the socket
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (socket) {
|
|
||||||
socket.on("ready", () => {
|
|
||||||
setIsSocketReady(true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (socket) socket.off("ready")
|
|
||||||
}
|
|
||||||
}, [socket])
|
|
||||||
|
|
||||||
const createNewTerminal = async (command?: string): Promise<void> => {
|
const createNewTerminal = async (command?: string): Promise<void> => {
|
||||||
if (!socket) return
|
if (!socket) return
|
||||||
@ -94,20 +78,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAppExists = async (
|
|
||||||
appName: string
|
|
||||||
): Promise<{ success: boolean; exists?: boolean }> => {
|
|
||||||
console.log("Is there a socket: " + !!socket)
|
|
||||||
if (!socket) {
|
|
||||||
console.error("Couldn't check if app exists: No socket")
|
|
||||||
return { success: false }
|
|
||||||
}
|
|
||||||
const response: { success: boolean; exists?: boolean } = await new Promise(
|
|
||||||
(resolve) => socket.emit("getAppExists", { appName }, resolve)
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
terminals,
|
terminals,
|
||||||
setTerminals,
|
setTerminals,
|
||||||
@ -118,7 +88,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
createNewTerminal,
|
createNewTerminal,
|
||||||
closeTerminal,
|
closeTerminal,
|
||||||
deploy,
|
deploy,
|
||||||
getAppExists: isSocketReady ? getAppExists : null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
import { revalidatePath } from "next/cache"
|
import { revalidatePath } from "next/cache"
|
||||||
import { z } from "zod"
|
|
||||||
import { editUserSchema } from "./schema"
|
|
||||||
import { UserLink } from "./types"
|
|
||||||
import { parseSocialLink } from "./utils"
|
|
||||||
|
|
||||||
export async function createSandbox(body: {
|
export async function createSandbox(body: {
|
||||||
type: string
|
type: string
|
||||||
@ -95,110 +91,3 @@ export async function unshareSandbox(sandboxId: string, userId: string) {
|
|||||||
|
|
||||||
revalidatePath(`/code/${sandboxId}`)
|
revalidatePath(`/code/${sandboxId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleLike(sandboxId: string, userId: string) {
|
|
||||||
const res = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/sandbox/like`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ sandboxId, userId }),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
revalidatePath(`/[username]`, "page")
|
|
||||||
revalidatePath(`/dashboard`, "page")
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpdateErrorSchema = z.object({
|
|
||||||
error: z
|
|
||||||
.union([
|
|
||||||
z.string(),
|
|
||||||
z.array(
|
|
||||||
z.object({
|
|
||||||
path: z.array(z.string()),
|
|
||||||
message: z.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
])
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface FormState {
|
|
||||||
message: string
|
|
||||||
error?: any
|
|
||||||
newRoute?: string
|
|
||||||
fields?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
export async function updateUser(
|
|
||||||
prevState: any,
|
|
||||||
formData: FormData
|
|
||||||
): Promise<FormState> {
|
|
||||||
let data = Object.fromEntries(formData)
|
|
||||||
let links: UserLink[] = []
|
|
||||||
Object.entries(data).forEach(([key, value]) => {
|
|
||||||
if (key.startsWith("link")) {
|
|
||||||
const [_, index] = key.split(".")
|
|
||||||
if (value) {
|
|
||||||
links.splice(parseInt(index), 0, parseSocialLink(value as string))
|
|
||||||
delete data[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// @ts-ignore
|
|
||||||
data.links = links
|
|
||||||
try {
|
|
||||||
const validatedData = editUserSchema.parse(data)
|
|
||||||
const changedUsername = validatedData.username !== validatedData.oldUsername
|
|
||||||
const res = await fetch(
|
|
||||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: validatedData.id,
|
|
||||||
username: data.username ?? undefined,
|
|
||||||
name: data.name ?? undefined,
|
|
||||||
bio: data.bio ?? undefined,
|
|
||||||
personalWebsite: data.personalWebsite ?? undefined,
|
|
||||||
links: data.links ?? undefined,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const responseData = await res.json()
|
|
||||||
|
|
||||||
// Validate the response using our error schema
|
|
||||||
const parseResult = UpdateErrorSchema.safeParse(responseData)
|
|
||||||
|
|
||||||
if (!parseResult.success) {
|
|
||||||
return {
|
|
||||||
message: "Unexpected error occurred",
|
|
||||||
error: parseResult.error,
|
|
||||||
fields: validatedData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changedUsername) {
|
|
||||||
const newRoute = `/@${validatedData.username}`
|
|
||||||
return { message: "Successfully updated", newRoute }
|
|
||||||
}
|
|
||||||
revalidatePath(`/[username]`, "page")
|
|
||||||
return { message: "Successfully updated" }
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
return {
|
|
||||||
message: "Invalid data",
|
|
||||||
error: error.errors,
|
|
||||||
fields: data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { message: "An unexpected error occurred", fields: data }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
export const KNOWN_PLATFORMS = [
|
|
||||||
"github",
|
|
||||||
"twitter",
|
|
||||||
"instagram",
|
|
||||||
"bluesky",
|
|
||||||
"linkedin",
|
|
||||||
"youtube",
|
|
||||||
"twitch",
|
|
||||||
"discord",
|
|
||||||
"mastodon",
|
|
||||||
"threads",
|
|
||||||
"gitlab",
|
|
||||||
"generic",
|
|
||||||
] as const
|
|
@ -1,37 +1,3 @@
|
|||||||
import {
|
|
||||||
AtSign,
|
|
||||||
Github,
|
|
||||||
GitlabIcon as GitlabLogo,
|
|
||||||
Globe,
|
|
||||||
Instagram,
|
|
||||||
Link,
|
|
||||||
Linkedin,
|
|
||||||
MessageCircle,
|
|
||||||
Twitch,
|
|
||||||
Twitter,
|
|
||||||
Youtube,
|
|
||||||
} from "lucide-react"
|
|
||||||
import { KnownPlatform } from "../types"
|
|
||||||
|
|
||||||
export const socialIcons: Record<
|
|
||||||
KnownPlatform | "website",
|
|
||||||
React.ComponentType<any>
|
|
||||||
> = {
|
|
||||||
github: Github,
|
|
||||||
twitter: Twitter,
|
|
||||||
instagram: Instagram,
|
|
||||||
bluesky: AtSign,
|
|
||||||
linkedin: Linkedin,
|
|
||||||
youtube: Youtube,
|
|
||||||
twitch: Twitch,
|
|
||||||
discord: MessageCircle,
|
|
||||||
mastodon: AtSign,
|
|
||||||
threads: AtSign,
|
|
||||||
gitlab: GitlabLogo,
|
|
||||||
generic: Link,
|
|
||||||
website: Globe,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const projectTemplates: {
|
export const projectTemplates: {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@ -67,11 +33,4 @@ export const projectTemplates: {
|
|||||||
description: "A faster way to build and share data apps",
|
description: "A faster way to build and share data apps",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "php",
|
|
||||||
name: "PHP",
|
|
||||||
description: "PHP development environment",
|
|
||||||
icon: "/project-icons/php.svg",
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
@ -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>
|
|
@ -1,290 +1,248 @@
|
|||||||
export interface TemplateConfig {
|
export interface TemplateConfig {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string,
|
||||||
runCommand: string
|
runCommand: string,
|
||||||
fileStructure: {
|
fileStructure: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
purpose: string
|
purpose: string
|
||||||
description: string
|
description: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conventions: string[]
|
||||||
|
dependencies?: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
scripts?: {
|
||||||
|
[key: string]: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
conventions: string[]
|
|
||||||
dependencies?: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
scripts?: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const templateConfigs: { [key: string]: TemplateConfig } = {
|
export const templateConfigs: { [key: string]: TemplateConfig } = {
|
||||||
reactjs: {
|
reactjs: {
|
||||||
id: "reactjs",
|
id: "reactjs",
|
||||||
name: "React",
|
name: "React",
|
||||||
runCommand: "npm run dev",
|
runCommand: "npm run dev",
|
||||||
fileStructure: {
|
fileStructure: {
|
||||||
"src/": {
|
"src/": {
|
||||||
purpose: "source",
|
purpose: "source",
|
||||||
description: "Contains all React components and application logic",
|
description: "Contains all React components and application logic"
|
||||||
|
},
|
||||||
|
"src/components/": {
|
||||||
|
purpose: "components",
|
||||||
|
description: "Reusable React components"
|
||||||
|
},
|
||||||
|
"src/lib/": {
|
||||||
|
purpose: "utilities",
|
||||||
|
description: "Utility functions and shared code"
|
||||||
|
},
|
||||||
|
"src/App.tsx": {
|
||||||
|
purpose: "entry",
|
||||||
|
description: "Main application component"
|
||||||
|
},
|
||||||
|
"src/index.tsx": {
|
||||||
|
purpose: "entry",
|
||||||
|
description: "Application entry point"
|
||||||
|
},
|
||||||
|
"src/index.css": {
|
||||||
|
purpose: "styles",
|
||||||
|
description: "Global CSS styles"
|
||||||
|
},
|
||||||
|
"public/": {
|
||||||
|
purpose: "static",
|
||||||
|
description: "Static assets and index.html"
|
||||||
|
},
|
||||||
|
"tsconfig.json": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "TypeScript configuration"
|
||||||
|
},
|
||||||
|
"vite.config.ts": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "Vite bundler configuration"
|
||||||
|
},
|
||||||
|
"package.json": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "Project dependencies and scripts"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"src/components/": {
|
conventions: [
|
||||||
purpose: "components",
|
"Use functional components with hooks",
|
||||||
description: "Reusable React components",
|
"Follow React naming conventions (PascalCase for components)",
|
||||||
},
|
"Keep components small and focused",
|
||||||
"src/lib/": {
|
"Use TypeScript for type safety"
|
||||||
purpose: "utilities",
|
],
|
||||||
description: "Utility functions and shared code",
|
dependencies: {
|
||||||
},
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"src/App.tsx": {
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
purpose: "entry",
|
"class-variance-authority": "^0.7.0",
|
||||||
description: "Main application component",
|
"clsx": "^2.1.1",
|
||||||
},
|
"lucide-react": "^0.441.0",
|
||||||
"src/index.tsx": {
|
"react": "^18.3.1",
|
||||||
purpose: "entry",
|
"react-dom": "^18.3.1",
|
||||||
description: "Application entry point",
|
"tailwind-merge": "^2.5.2",
|
||||||
},
|
"tailwindcss-animate": "^1.0.7"
|
||||||
"src/index.css": {
|
|
||||||
purpose: "styles",
|
|
||||||
description: "Global CSS styles",
|
|
||||||
},
|
|
||||||
"public/": {
|
|
||||||
purpose: "static",
|
|
||||||
description: "Static assets and index.html",
|
|
||||||
},
|
|
||||||
"tsconfig.json": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "TypeScript configuration",
|
|
||||||
},
|
|
||||||
"vite.config.ts": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Vite bundler configuration",
|
|
||||||
},
|
|
||||||
"package.json": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Project dependencies and scripts",
|
|
||||||
},
|
},
|
||||||
|
scripts: {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
conventions: [
|
// Next.js template config
|
||||||
"Use functional components with hooks",
|
nextjs: {
|
||||||
"Follow React naming conventions (PascalCase for components)",
|
id: "nextjs",
|
||||||
"Keep components small and focused",
|
name: "NextJS",
|
||||||
"Use TypeScript for type safety",
|
runCommand: "npm run dev",
|
||||||
],
|
fileStructure: {
|
||||||
dependencies: {
|
"pages/": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
purpose: "routing",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
description: "Page components and API routes"
|
||||||
"class-variance-authority": "^0.7.0",
|
},
|
||||||
clsx: "^2.1.1",
|
"pages/api/": {
|
||||||
"lucide-react": "^0.441.0",
|
purpose: "api",
|
||||||
react: "^18.3.1",
|
description: "API route handlers"
|
||||||
"react-dom": "^18.3.1",
|
},
|
||||||
"tailwind-merge": "^2.5.2",
|
"pages/_app.tsx": {
|
||||||
"tailwindcss-animate": "^1.0.7",
|
purpose: "entry",
|
||||||
|
description: "Application wrapper component"
|
||||||
|
},
|
||||||
|
"pages/index.tsx": {
|
||||||
|
purpose: "page",
|
||||||
|
description: "Homepage component"
|
||||||
|
},
|
||||||
|
"public/": {
|
||||||
|
purpose: "static",
|
||||||
|
description: "Static assets and files"
|
||||||
|
},
|
||||||
|
"styles/": {
|
||||||
|
purpose: "styles",
|
||||||
|
description: "CSS modules and global styles"
|
||||||
|
},
|
||||||
|
"styles/globals.css": {
|
||||||
|
purpose: "styles",
|
||||||
|
description: "Global CSS styles"
|
||||||
|
},
|
||||||
|
"styles/Home.module.css": {
|
||||||
|
purpose: "styles",
|
||||||
|
description: "Homepage-specific styles"
|
||||||
|
},
|
||||||
|
"next.config.js": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "Next.js configuration"
|
||||||
|
},
|
||||||
|
"next-env.d.ts": {
|
||||||
|
purpose: "types",
|
||||||
|
description: "Next.js TypeScript declarations"
|
||||||
|
},
|
||||||
|
"tsconfig.json": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "TypeScript configuration"
|
||||||
|
},
|
||||||
|
"package.json": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "Project dependencies and scripts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
conventions: [
|
||||||
|
"Use file-system based routing",
|
||||||
|
"Keep API routes in pages/api",
|
||||||
|
"Use CSS Modules for component styles",
|
||||||
|
"Follow Next.js data fetching patterns"
|
||||||
|
],
|
||||||
|
dependencies: {
|
||||||
|
"next": "^14.1.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"tailwindcss": "^3.4.1"
|
||||||
|
},
|
||||||
|
scripts: {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scripts: {
|
// Streamlit template config
|
||||||
dev: "vite",
|
streamlit: {
|
||||||
build: "tsc && vite build",
|
id: "streamlit",
|
||||||
preview: "vite preview",
|
name: "Streamlit",
|
||||||
|
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
||||||
|
fileStructure: {
|
||||||
|
"main.py": {
|
||||||
|
purpose: "entry",
|
||||||
|
description: "Main Streamlit application file"
|
||||||
|
},
|
||||||
|
"requirements.txt": {
|
||||||
|
purpose: "dependencies",
|
||||||
|
description: "Python package dependencies"
|
||||||
|
},
|
||||||
|
"Procfile": {
|
||||||
|
purpose: "deployment",
|
||||||
|
description: "Deployment configuration for hosting platforms"
|
||||||
|
},
|
||||||
|
"venv/": {
|
||||||
|
purpose: "environment",
|
||||||
|
description: "Python virtual environment directory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
conventions: [
|
||||||
|
"Use Streamlit components for UI",
|
||||||
|
"Follow PEP 8 style guide",
|
||||||
|
"Keep dependencies in requirements.txt",
|
||||||
|
"Use virtual environment for isolation"
|
||||||
|
],
|
||||||
|
dependencies: {
|
||||||
|
"streamlit": "^1.40.0",
|
||||||
|
"altair": "^5.5.0"
|
||||||
|
},
|
||||||
|
scripts: {
|
||||||
|
"start": "streamlit run main.py",
|
||||||
|
"dev": "./venv/bin/streamlit run main.py --server.runOnSave true"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
// HTML template config
|
||||||
// Next.js template config
|
vanillajs: {
|
||||||
nextjs: {
|
id: "vanillajs",
|
||||||
id: "nextjs",
|
name: "HTML/JS",
|
||||||
name: "NextJS",
|
runCommand: "npm run dev",
|
||||||
runCommand: "npm run dev",
|
fileStructure: {
|
||||||
fileStructure: {
|
"index.html": {
|
||||||
"pages/": {
|
purpose: "entry",
|
||||||
purpose: "routing",
|
description: "Main HTML entry point"
|
||||||
description: "Page components and API routes",
|
},
|
||||||
|
"style.css": {
|
||||||
|
purpose: "styles",
|
||||||
|
description: "Global CSS styles"
|
||||||
|
},
|
||||||
|
"script.js": {
|
||||||
|
purpose: "scripts",
|
||||||
|
description: "JavaScript application logic"
|
||||||
|
},
|
||||||
|
"package.json": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "Project dependencies and scripts"
|
||||||
|
},
|
||||||
|
"package-lock.json": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "Locked dependency versions"
|
||||||
|
},
|
||||||
|
"vite.config.js": {
|
||||||
|
purpose: "config",
|
||||||
|
description: "Vite bundler configuration"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"pages/api/": {
|
conventions: [
|
||||||
purpose: "api",
|
"Use semantic HTML elements",
|
||||||
description: "API route handlers",
|
"Keep CSS modular and organized",
|
||||||
|
"Write clean, modular JavaScript",
|
||||||
|
"Follow modern ES6+ practices"
|
||||||
|
],
|
||||||
|
dependencies: {
|
||||||
|
"vite": "^5.0.12"
|
||||||
},
|
},
|
||||||
"pages/_app.tsx": {
|
scripts: {
|
||||||
purpose: "entry",
|
"dev": "vite",
|
||||||
description: "Application wrapper component",
|
"build": "vite build",
|
||||||
},
|
"preview": "vite preview"
|
||||||
"pages/index.tsx": {
|
}
|
||||||
purpose: "page",
|
}
|
||||||
description: "Homepage component",
|
|
||||||
},
|
|
||||||
"public/": {
|
|
||||||
purpose: "static",
|
|
||||||
description: "Static assets and files",
|
|
||||||
},
|
|
||||||
"styles/": {
|
|
||||||
purpose: "styles",
|
|
||||||
description: "CSS modules and global styles",
|
|
||||||
},
|
|
||||||
"styles/globals.css": {
|
|
||||||
purpose: "styles",
|
|
||||||
description: "Global CSS styles",
|
|
||||||
},
|
|
||||||
"styles/Home.module.css": {
|
|
||||||
purpose: "styles",
|
|
||||||
description: "Homepage-specific styles",
|
|
||||||
},
|
|
||||||
"next.config.js": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Next.js configuration",
|
|
||||||
},
|
|
||||||
"next-env.d.ts": {
|
|
||||||
purpose: "types",
|
|
||||||
description: "Next.js TypeScript declarations",
|
|
||||||
},
|
|
||||||
"tsconfig.json": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "TypeScript configuration",
|
|
||||||
},
|
|
||||||
"package.json": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Project dependencies and scripts",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
conventions: [
|
|
||||||
"Use file-system based routing",
|
|
||||||
"Keep API routes in pages/api",
|
|
||||||
"Use CSS Modules for component styles",
|
|
||||||
"Follow Next.js data fetching patterns",
|
|
||||||
],
|
|
||||||
dependencies: {
|
|
||||||
next: "^14.1.0",
|
|
||||||
react: "^18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
tailwindcss: "^3.4.1",
|
|
||||||
},
|
|
||||||
scripts: {
|
|
||||||
dev: "next dev",
|
|
||||||
build: "next build",
|
|
||||||
start: "next start",
|
|
||||||
lint: "next lint",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Streamlit template config
|
|
||||||
streamlit: {
|
|
||||||
id: "streamlit",
|
|
||||||
name: "Streamlit",
|
|
||||||
runCommand: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
|
||||||
fileStructure: {
|
|
||||||
"main.py": {
|
|
||||||
purpose: "entry",
|
|
||||||
description: "Main Streamlit application file",
|
|
||||||
},
|
|
||||||
"requirements.txt": {
|
|
||||||
purpose: "dependencies",
|
|
||||||
description: "Python package dependencies",
|
|
||||||
},
|
|
||||||
Procfile: {
|
|
||||||
purpose: "deployment",
|
|
||||||
description: "Deployment configuration for hosting platforms",
|
|
||||||
},
|
|
||||||
"venv/": {
|
|
||||||
purpose: "environment",
|
|
||||||
description: "Python virtual environment directory",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
conventions: [
|
|
||||||
"Use Streamlit components for UI",
|
|
||||||
"Follow PEP 8 style guide",
|
|
||||||
"Keep dependencies in requirements.txt",
|
|
||||||
"Use virtual environment for isolation",
|
|
||||||
],
|
|
||||||
dependencies: {
|
|
||||||
streamlit: "^1.40.0",
|
|
||||||
altair: "^5.5.0",
|
|
||||||
},
|
|
||||||
scripts: {
|
|
||||||
start: "streamlit run main.py",
|
|
||||||
dev: "./venv/bin/streamlit run main.py --server.runOnSave true",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// HTML template config
|
|
||||||
vanillajs: {
|
|
||||||
id: "vanillajs",
|
|
||||||
name: "HTML/JS",
|
|
||||||
runCommand: "npm run dev",
|
|
||||||
fileStructure: {
|
|
||||||
"index.html": {
|
|
||||||
purpose: "entry",
|
|
||||||
description: "Main HTML entry point",
|
|
||||||
},
|
|
||||||
"style.css": {
|
|
||||||
purpose: "styles",
|
|
||||||
description: "Global CSS styles",
|
|
||||||
},
|
|
||||||
"script.js": {
|
|
||||||
purpose: "scripts",
|
|
||||||
description: "JavaScript application logic",
|
|
||||||
},
|
|
||||||
"package.json": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Project dependencies and scripts",
|
|
||||||
},
|
|
||||||
"package-lock.json": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Locked dependency versions",
|
|
||||||
},
|
|
||||||
"vite.config.js": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Vite bundler configuration",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
conventions: [
|
|
||||||
"Use semantic HTML elements",
|
|
||||||
"Keep CSS modular and organized",
|
|
||||||
"Write clean, modular JavaScript",
|
|
||||||
"Follow modern ES6+ practices",
|
|
||||||
],
|
|
||||||
dependencies: {
|
|
||||||
vite: "^5.0.12",
|
|
||||||
},
|
|
||||||
scripts: {
|
|
||||||
dev: "vite",
|
|
||||||
build: "vite build",
|
|
||||||
preview: "vite preview",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// PHP template config
|
|
||||||
php: {
|
|
||||||
id: "php",
|
|
||||||
name: "PHP",
|
|
||||||
runCommand: "npx vite",
|
|
||||||
fileStructure: {
|
|
||||||
"index.php": {
|
|
||||||
purpose: "entry",
|
|
||||||
description: "Main PHP entry point",
|
|
||||||
},
|
|
||||||
"package.json": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Frontend dependencies and scripts",
|
|
||||||
},
|
|
||||||
"package-lock.json": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Locked dependency versions",
|
|
||||||
},
|
|
||||||
"vite.config.js": {
|
|
||||||
purpose: "config",
|
|
||||||
description: "Vite configuration for frontend assets",
|
|
||||||
},
|
|
||||||
"node_modules/": {
|
|
||||||
purpose: "dependencies",
|
|
||||||
description: "Frontend dependency files",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
conventions: [
|
|
||||||
"Follow PSR-12 coding standards",
|
|
||||||
"Use modern PHP 8+ features",
|
|
||||||
"Organize assets with Vite",
|
|
||||||
"Keep PHP logic separate from presentation",
|
|
||||||
],
|
|
||||||
dependencies: {
|
|
||||||
vite: "^5.0.0",
|
|
||||||
},
|
|
||||||
scripts: {
|
|
||||||
dev: "vite",
|
|
||||||
build: "vite build",
|
|
||||||
preview: "vite preview",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
export const TIERS = {
|
export const TIERS = {
|
||||||
FREE: {
|
FREE: {
|
||||||
// generations: 100,
|
// generations: 100,
|
||||||
// maxTokens: 1024,
|
// maxTokens: 1024,
|
||||||
generations: 1000,
|
generations: 1000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
model: "claude-3-5-sonnet-20240620",
|
model: "claude-3-5-sonnet-20240620",
|
||||||
},
|
},
|
||||||
PRO: {
|
PRO: {
|
||||||
generations: 500,
|
generations: 500,
|
||||||
maxTokens: 2048,
|
maxTokens: 2048,
|
||||||
model: "claude-3-5-sonnet-20240620",
|
model: "claude-3-5-sonnet-20240620",
|
||||||
},
|
},
|
||||||
ENTERPRISE: {
|
ENTERPRISE: {
|
||||||
generations: 1000,
|
generations: 1000,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
model: "claude-3-5-sonnet-20240620",
|
model: "claude-3-5-sonnet-20240620",
|
||||||
},
|
},
|
||||||
}
|
}
|
@ -1,7 +1,5 @@
|
|||||||
// DB Types
|
// DB Types
|
||||||
|
|
||||||
import { KNOWN_PLATFORMS } from "./constants"
|
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@ -10,20 +8,11 @@ export type User = {
|
|||||||
avatarUrl: string | null
|
avatarUrl: string | null
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
generations: number
|
generations: number
|
||||||
bio: string | null
|
|
||||||
personalWebsite: string | null
|
|
||||||
links: UserLink[]
|
|
||||||
tier: "FREE" | "PRO" | "ENTERPRISE"
|
|
||||||
tierExpiresAt: Date
|
|
||||||
lastResetDate: number
|
|
||||||
sandbox: Sandbox[]
|
sandbox: Sandbox[]
|
||||||
usersToSandboxes: UsersToSandboxes[]
|
usersToSandboxes: UsersToSandboxes[]
|
||||||
}
|
tier: "FREE" | "PRO" | "ENTERPRISE"
|
||||||
|
tierExpiresAt: Date
|
||||||
export type KnownPlatform = (typeof KNOWN_PLATFORMS)[number]
|
lastResetDate?: number
|
||||||
export type UserLink = {
|
|
||||||
url: string
|
|
||||||
platform: KnownPlatform
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Sandbox = {
|
export type Sandbox = {
|
||||||
@ -33,13 +22,9 @@ export type Sandbox = {
|
|||||||
visibility: "public" | "private"
|
visibility: "public" | "private"
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
userId: string
|
userId: string
|
||||||
likeCount: number
|
|
||||||
viewCount: number
|
|
||||||
usersToSandboxes: UsersToSandboxes[]
|
usersToSandboxes: UsersToSandboxes[]
|
||||||
}
|
}
|
||||||
export type SandboxWithLiked = Sandbox & {
|
|
||||||
liked: boolean
|
|
||||||
}
|
|
||||||
export type UsersToSandboxes = {
|
export type UsersToSandboxes = {
|
||||||
userId: string
|
userId: string
|
||||||
sandboxId: string
|
sandboxId: string
|
||||||
|
@ -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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
1317
frontend/package-lock.json
generated
1317
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,7 @@
|
|||||||
"@clerk/nextjs": "^4.29.12",
|
"@clerk/nextjs": "^4.29.12",
|
||||||
"@clerk/themes": "^1.7.12",
|
"@clerk/themes": "^1.7.12",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@hookform/resolvers": "^3.9.1",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@liveblocks/client": "^1.12.0",
|
"@liveblocks/client": "^1.12.0",
|
||||||
"@liveblocks/node": "^1.12.0",
|
"@liveblocks/node": "^1.12.0",
|
||||||
"@liveblocks/react": "^1.12.0",
|
"@liveblocks/react": "^1.12.0",
|
||||||
@ -22,21 +22,17 @@
|
|||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@paralleldrive/cuid2": "^2.2.2",
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
|
||||||
"@radix-ui/react-context-menu": "^2.1.5",
|
"@radix-ui/react-context-menu": "^2.1.5",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-hover-card": "^1.1.2",
|
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.1.1",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-progress": "^1.1.0",
|
"@radix-ui/react-progress": "^1.1.0",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.1.6",
|
|
||||||
"@react-three/fiber": "^8.16.6",
|
"@react-three/fiber": "^8.16.6",
|
||||||
"@uiw/codemirror-theme-vscode": "^4.23.5",
|
"@uiw/codemirror-theme-vscode": "^4.23.5",
|
||||||
"@uiw/react-codemirror": "^4.23.5",
|
"@uiw/react-codemirror": "^4.23.5",
|
||||||
@ -55,12 +51,11 @@
|
|||||||
"lucide-react": "^0.365.0",
|
"lucide-react": "^0.365.0",
|
||||||
"monaco-themes": "^0.4.4",
|
"monaco-themes": "^0.4.4",
|
||||||
"next": "14.1.3",
|
"next": "14.1.3",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.3.0",
|
||||||
"openai": "^4.73.1",
|
|
||||||
"posthog-js": "^1.147.0",
|
"posthog-js": "^1.147.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.51.3",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-resizable-panels": "^2.0.16",
|
"react-resizable-panels": "^2.0.16",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
@ -75,7 +70,7 @@
|
|||||||
"y-monaco": "^0.1.5",
|
"y-monaco": "^0.1.5",
|
||||||
"y-protocols": "^1.0.6",
|
"y-protocols": "^1.0.6",
|
||||||
"yjs": "^13.6.15",
|
"yjs": "^13.6.15",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.6",
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="16" cy="16" r="14" fill="#8892BF"/>
|
|
||||||
<path d="M14.4392 10H16.1192L15.6444 12.5242H17.154C17.9819 12.5419 18.5986 12.7269 19.0045 13.0793C19.4184 13.4316 19.5402 14.1014 19.3698 15.0881L18.5541 19.4889H16.8497L17.6288 15.2863C17.7099 14.8457 17.6856 14.533 17.5558 14.348C17.426 14.163 17.146 14.0705 16.7158 14.0705L15.3644 14.0573L14.3661 19.4889H12.6861L14.4392 10Z" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.74092 12.5243H10.0036C10.9612 12.533 11.6552 12.8327 12.0854 13.4229C12.5156 14.0132 12.6576 14.8193 12.5115 15.8414C12.4548 16.3085 12.3289 16.7665 12.1341 17.2159C11.9474 17.6652 11.6878 18.0704 11.355 18.4317C10.9491 18.8898 10.5149 19.1805 10.0523 19.304C9.58969 19.4274 9.11076 19.489 8.61575 19.489H7.15484L6.69222 22H5L6.74092 12.5243ZM7.43485 17.9956L8.16287 14.0441H8.40879C8.49815 14.0441 8.5914 14.0396 8.6888 14.0309C9.33817 14.0221 9.87774 14.0882 10.308 14.2291C10.7462 14.37 10.8923 14.9031 10.7462 15.8282C10.5678 16.9296 10.2186 17.5727 9.69926 17.7577C9.1799 17.934 8.53053 18.0176 7.75138 18.0088H7.58094C7.53224 18.0088 7.48355 18.0043 7.43485 17.9956Z" fill="white"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4365 12.5243H21.1738L19.4329 22H21.1251L21.5878 19.489H23.0487C23.5437 19.489 24.0226 19.4274 24.4852 19.304C24.9479 19.1805 25.382 18.8898 25.7879 18.4317C26.1207 18.0704 26.3803 17.6652 26.567 17.2159C26.7618 16.7665 26.8877 16.3085 26.9444 15.8414C27.0905 14.8193 26.9486 14.0132 26.5183 13.4229C26.0881 12.8327 25.3942 12.533 24.4365 12.5243ZM22.5958 14.0441L21.8678 17.9956C21.9165 18.0043 21.9652 18.0088 22.0139 18.0088H22.1843C22.9635 18.0176 23.6128 17.934 24.1322 17.7577C24.6515 17.5727 25.0007 16.9296 25.1792 15.8282C25.3253 14.9031 25.1792 14.37 24.7409 14.2291C24.3107 14.0882 23.7711 14.0221 23.1217 14.0309C23.0243 14.0396 22.9311 14.0441 22.8417 14.0441H22.5958Z" fill="white"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 2.1 KiB |
Loading…
x
Reference in New Issue
Block a user