Compare commits

...

6 Commits

Author SHA1 Message Date
Akhileshrangani4
0828209455 Chore: Change about to help, and add link to Discord 2024-11-17 18:10:19 -05:00
Akhileshrangani4
94ca5b2c9f fix: comment out live collaboration features 2024-11-17 17:52:39 -05:00
Akhileshrangani4
2a58d0a5e3 fix: type errors, shared page avatars and project icons 2024-11-11 16:02:26 -05:00
Akhileshrangani4
30c9da559f feat: user avatar images
- added user avatars for each user
- it will fetch user images from github or google and if there is no image then it will show initials
2024-11-11 16:01:47 -05:00
Akhileshrangani4
2262adca74 feat: schema updates
- added additional items to users and sandbox tables
- added a random username generator
2024-11-10 21:52:52 -05:00
Akhileshrangani4
b486d22111 chore: remove unnecessary logs 2024-11-09 17:57:48 -05:00
36 changed files with 10854 additions and 8554 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@
"drizzle-kit": "^0.20.17", "drizzle-kit": "^0.20.17",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"vitest": "1.3.0", "vitest": "1.3.0",
"wrangler": "^3.0.0" "wrangler": "^3.86.0"
}, },
"dependencies": { "dependencies": {
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,3 +1,4 @@
import { ExecutionContext, R2Bucket, Headers as CFHeaders } from "@cloudflare/workers-types"
import { z } from "zod" import { z } from "zod"
export interface Env { export interface Env {
@ -75,14 +76,13 @@ export default {
if (obj === null) { if (obj === null) {
return new Response(`${fileId} not found`, { status: 404 }) return new Response(`${fileId} not found`, { status: 404 })
} }
const headers = new Headers() const headers = new Headers() as unknown as CFHeaders
headers.set("etag", obj.httpEtag) headers.set("etag", obj.httpEtag)
obj.writeHttpMetadata(headers) obj.writeHttpMetadata(headers)
const text = await obj.text() const text = await obj.text()
return new Response(text, { return new Response(text, {
headers, headers: Object.fromEntries(headers.entries()),
}) })
} else return invalidRequest } else return invalidRequest
} else if (method === "POST") { } else if (method === "POST") {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -27,9 +27,11 @@
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.1.3",
"@react-three/fiber": "^8.16.6", "@react-three/fiber": "^8.16.6",
"@uiw/codemirror-theme-vscode": "^4.23.5", "@uiw/codemirror-theme-vscode": "^4.23.5",
"@uiw/react-codemirror": "^4.23.5", "@uiw/react-codemirror": "^4.23.5",
@ -57,6 +59,7 @@
"react-resizable-panels": "^2.0.16", "react-resizable-panels": "^2.0.16",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"shadcn": "^2.1.6",
"socket.io-client": "^4.7.5", "socket.io-client": "^4.7.5",
"sonner": "^1.4.41", "sonner": "^1.4.41",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.3.0",