feat: add light theme WIP

This commit is contained in:
Hamzat Victor 2024-10-23 10:51:50 +01:00
parent 68964c2c8f
commit eb4e34cf10
11 changed files with 302 additions and 179 deletions

View File

@ -1,4 +1,4 @@
frontend/**
# frontend/**
backend/ai/**
backend/database/**
backend/storage/**

View File

@ -99,6 +99,29 @@
); /* violet 900 -> bg */
}
.light .gradient-button-bg {
background: radial-gradient(
circle at top,
#262626 0%,
#f5f5f5 50%
); /* Dark gray -> Light gray */
}
.light .gradient-button {
background: radial-gradient(
circle at bottom,
hsl(0, 10%, 25%) -10%,
#9d9d9d 50%
); /* Light gray -> Almost white */
}
.light .gradient-button-bg > div:hover {
background: radial-gradient(
circle at bottom,
hsl(0, 10%, 25%) -10%,
#9d9d9d 80%
); /* Light gray -> Almost white */
}
.inline-decoration::before {
content: "Generate";
color: #525252;

View File

@ -1,13 +1,13 @@
import type { Metadata } from "next"
import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
import "./globals.css"
import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner"
import { Analytics } from "@vercel/analytics/react"
import { PreviewProvider } from "@/context/PreviewContext";
import { ThemeProvider } from "@/components/ui/theme-provider"
import { PreviewProvider } from "@/context/PreviewContext"
import { SocketProvider } from '@/context/SocketContext'
import { ClerkProvider } from "@clerk/nextjs"
import { Analytics } from "@vercel/analytics/react"
import { GeistMono } from "geist/font/mono"
import { GeistSans } from "geist/font/sans"
import type { Metadata } from "next"
import "./globals.css"
export const metadata: Metadata = {
title: "Sandbox",
@ -25,8 +25,7 @@ export default function RootLayout({
<body>
<ThemeProvider
attribute="class"
defaultTheme="dark"
forcedTheme="dark"
defaultTheme="system"
disableTransitionOnChange
>
<SocketProvider>

View File

@ -3,16 +3,14 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import Image from "next/image"
import { useState, useCallback, useEffect, useMemo } from "react"
import { set, z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import Image from "next/image"
import { useCallback, useEffect, useMemo, useState } from "react"
import { useForm } from "react-hook-form"
import { z } from "zod"
import {
Form,
@ -31,23 +29,17 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { useUser } from "@clerk/nextjs"
import { createSandbox } from "@/lib/actions"
import { useRouter } from "next/navigation"
import {
Loader2,
ChevronRight,
ChevronLeft,
Search,
SlashSquare,
} from "lucide-react"
import { Button } from "../ui/button"
import { projectTemplates } from "@/lib/data"
import { useUser } from "@clerk/nextjs"
import { ChevronLeft, ChevronRight, Loader2, Search } from "lucide-react"
import { useRouter } from "next/navigation"
import { Button } from "../ui/button"
import useEmblaCarousel from "embla-carousel-react"
import type { EmblaCarouselType } from "embla-carousel"
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
import { cn } from "@/lib/utils"
import type { EmblaCarouselType } from "embla-carousel"
import useEmblaCarousel from "embla-carousel-react"
import { WheelGesturesPlugin } from "embla-carousel-wheel-gestures"
const formSchema = z.object({
name: z
.string()
@ -296,7 +288,7 @@ function SearchInput({
<form {...{ onSubmit }} className="w-40 h-8 ">
<label
htmlFor="template-search"
className="flex gap-2 rounded-sm transition-colors bg-[#2e2e2e] border border-[--s-color] [--s-color:hsl(var(--muted-foreground))] focus-within:[--s-color:#fff] h-full items-center px-2"
className="flex gap-2 rounded-sm transition-colors bg-gray-100 dark:bg-[#2e2e2e] border border-[--s-color] [--s-color:hsl(var(--muted-foreground))] focus-within:[--s-color:#fff] h-full items-center px-2"
>
<Search className="size-4 text-[--s-color] transition-colors" />
<input

View File

@ -1,43 +1,57 @@
"use client"
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import * as monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { AnimatePresence, motion } from "framer-motion"
import * as monaco from "monaco-editor"
import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner"
import * as Y from "yjs"
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness"
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
import * as Y from "yjs"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { FileJson, Loader2, Sparkles, TerminalSquare, ArrowDownToLine, ArrowRightToLine } from "lucide-react"
import Tab from "../ui/tab"
import Sidebar from "./sidebar"
import GenerateInput from "./generate"
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
import { addNew, processFileType, validateName, debounce } from "@/lib/utils"
import { Cursors } from "./live/cursors"
import { PreviewProvider, usePreview } from "@/context/PreviewContext"
import { useSocket } from "@/context/SocketContext"
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
import { Sandbox, TFile, TFolder, TTab, User } from "@/lib/types"
import {
addNew,
cn,
debounce,
deepMerge,
processFileType,
validateName,
} from "@/lib/utils"
import { Terminal } from "@xterm/xterm"
import {
ArrowDownToLine,
ArrowRightToLine,
FileJson,
Loader2,
Sparkles,
TerminalSquare,
} from "lucide-react"
import { useTheme } from "next-themes"
import React from "react"
import { ImperativePanelHandle } from "react-resizable-panels"
import { Button } from "../ui/button"
import Tab from "../ui/tab"
import AIChat from "./AIChat"
import GenerateInput from "./generate"
import { Cursors } from "./live/cursors"
import DisableAccessModal from "./live/disableModal"
import Loading from "./loading"
import PreviewWindow from "./preview"
import Sidebar from "./sidebar"
import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from "@/context/PreviewContext"
import { useSocket } from "@/context/SocketContext"
import { Button } from "../ui/button"
import React from "react"
import { parseTSConfigToMonacoOptions } from "@/lib/tsconfig"
import { cn, deepMerge } from "@/lib/utils"
import AIChat from "./AIChat"
export default function CodeEditor({
userData,
@ -48,7 +62,8 @@ export default function CodeEditor({
}) {
//SocketContext functions and effects
const { socket, setUserAndSandboxId } = useSocket()
// theme
const { theme } = useTheme()
useEffect(() => {
// Ensure userData.id and sandboxData.id are available before attempting to connect
if (userData.id && sandboxData.id) {
@ -75,10 +90,10 @@ export default function CodeEditor({
})
// Layout state
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false);
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false)
// AI Chat state
const [isAIChatOpen, setIsAIChatOpen] = useState(false);
const [isAIChatOpen, setIsAIChatOpen] = useState(false)
// File state
const [files, setFiles] = useState<(TFolder | TFile)[]>([])
@ -91,6 +106,7 @@ export default function CodeEditor({
// Editor state
const [editorLanguage, setEditorLanguage] = useState("plaintext")
console.log("editor language: ",editorLanguage)
const [cursorLine, setCursorLine] = useState(0)
const [editorRef, setEditorRef] =
useState<monaco.editor.IStandaloneCodeEditor>()
@ -152,7 +168,7 @@ export default function CodeEditor({
const generateRef = useRef<HTMLDivElement>(null)
const suggestionRef = useRef<HTMLDivElement>(null)
const generateWidgetRef = useRef<HTMLDivElement>(null)
const { previewPanelRef } = usePreview();
const { previewPanelRef } = usePreview()
const editorPanelRef = useRef<ImperativePanelHandle>(null)
const previewWindowRef = useRef<{ refreshIframe: () => void }>(null)
@ -526,10 +542,10 @@ export default function CodeEditor({
const down = (e: KeyboardEvent) => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
debouncedSaveData(activeFileId);
debouncedSaveData(activeFileId)
} else if (e.key === "l" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setIsAIChatOpen(prev => !prev);
setIsAIChatOpen((prev) => !prev)
}
}
document.addEventListener("keydown", down)
@ -843,17 +859,17 @@ export default function CodeEditor({
const togglePreviewPanel = () => {
if (isPreviewCollapsed) {
previewPanelRef.current?.expand();
setIsPreviewCollapsed(false);
previewPanelRef.current?.expand()
setIsPreviewCollapsed(false)
} else {
previewPanelRef.current?.collapse();
setIsPreviewCollapsed(true);
previewPanelRef.current?.collapse()
setIsPreviewCollapsed(true)
}
}
};
const toggleLayout = () => {
setIsHorizontalLayout(prev => !prev);
};
setIsHorizontalLayout((prev) => !prev)
}
// On disabled access for shared users, show un-interactable loading placeholder + info modal
if (disableAccess.isDisabled)
@ -994,7 +1010,9 @@ export default function CodeEditor({
<ResizablePanelGroup direction="horizontal">
{/* Left side: Editor and Preview/Terminal */}
<ResizablePanel defaultSize={isAIChatOpen ? 80 : 100} minSize={50}>
<ResizablePanelGroup direction={isHorizontalLayout ? "vertical" : "horizontal"}>
<ResizablePanelGroup
direction={isHorizontalLayout ? "vertical" : "horizontal"}
>
<ResizablePanel
className="p-2 flex flex-col"
maxSize={80}
@ -1044,7 +1062,7 @@ export default function CodeEditor({
onChange={(value) => {
// If the new content is different from the cached content, update it
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
setTabs((prev) =>
prev.map((tab) =>
@ -1077,7 +1095,7 @@ export default function CodeEditor({
fixedOverflowWidgets: true,
fontFamily: "var(--font-geist-mono)",
}}
theme="vs-dark"
theme={theme === "light" ? "vs" : "vs-dark"}
value={activeFileContent}
/>
</>
@ -1091,25 +1109,39 @@ export default function CodeEditor({
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={30}>
<ResizablePanelGroup direction={
isAIChatOpen && isHorizontalLayout ? "horizontal" :
isAIChatOpen ? "vertical" :
isHorizontalLayout ? "horizontal" :
"vertical"
}>
<ResizablePanelGroup
direction={
isAIChatOpen && isHorizontalLayout
? "horizontal"
: isAIChatOpen
? "vertical"
: isHorizontalLayout
? "horizontal"
: "vertical"
}
>
<ResizablePanel
ref={previewPanelRef}
defaultSize={isPreviewCollapsed ? 4 : 20}
minSize={25}
collapsedSize={isHorizontalLayout ? 20 : 4}
className="p-2 flex flex-col"
className="p-2 flex flex-col gap-2"
collapsible
onCollapse={() => setIsPreviewCollapsed(true)}
onExpand={() => setIsPreviewCollapsed(false)}
>
<div className="flex items-center justify-between">
<Button onClick={toggleLayout} size="sm" variant="ghost" className="mr-2 border">
{isHorizontalLayout ? <ArrowRightToLine className="w-4 h-4" /> : <ArrowDownToLine className="w-4 h-4" />}
<Button
onClick={toggleLayout}
size="sm"
variant="ghost"
className="mr-2 border"
>
{isHorizontalLayout ? (
<ArrowRightToLine className="w-4 h-4" />
) : (
<ArrowDownToLine className="w-4 h-4" />
)}
</Button>
<PreviewWindow
open={togglePreviewPanel}
@ -1154,7 +1186,10 @@ export default function CodeEditor({
<ResizablePanel defaultSize={30} minSize={15}>
<AIChat
activeFileContent={activeFileContent}
activeFileName={tabs.find(tab => tab.id === activeFileId)?.name || 'No file selected'}
activeFileName={
tabs.find((tab) => tab.id === activeFileId)?.name ||
"No file selected"
}
/>
</ResizablePanel>
</>

View File

@ -1,33 +1,34 @@
"use client";
"use client"
import Image from "next/image";
import Logo from "@/assets/logo.svg";
import { Pencil, Users } from "lucide-react";
import Link from "next/link";
import { Sandbox, User } from "@/lib/types";
import UserButton from "@/components/ui/userButton";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import EditSandboxModal from "./edit";
import ShareSandboxModal from "./share";
import { Avatars } from "../live/avatars";
import RunButtonModal from "./run";
import DeployButtonModal from "./deploy";
import Logo from "@/assets/logo.svg"
import { Button } from "@/components/ui/button"
import { ThemeSwitcher } from "@/components/ui/theme-switcher"
import UserButton from "@/components/ui/userButton"
import { Sandbox, User } from "@/lib/types"
import { Pencil, Users } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { useState } from "react"
import { Avatars } from "../live/avatars"
import DeployButtonModal from "./deploy"
import EditSandboxModal from "./edit"
import RunButtonModal from "./run"
import ShareSandboxModal from "./share"
export default function Navbar({
userData,
sandboxData,
shared,
}: {
userData: User;
sandboxData: Sandbox;
shared: { id: string; name: string }[];
userData: User
sandboxData: Sandbox
shared: { id: string; name: string }[]
}) {
const [isEditOpen, setIsEditOpen] = useState(false);
const [isShareOpen, setIsShareOpen] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false)
const [isShareOpen, setIsShareOpen] = useState(false)
const [isRunning, setIsRunning] = useState(false)
const isOwner = sandboxData.userId === userData.id;;
const isOwner = sandboxData.userId === userData.id
return (
<>
@ -72,19 +73,17 @@ export default function Navbar({
{isOwner ? (
<>
<DeployButtonModal
data={sandboxData}
userData={userData}
/>
<DeployButtonModal data={sandboxData} userData={userData} />
<Button variant="outline" onClick={() => setIsShareOpen(true)}>
<Users className="w-4 h-4 mr-2" />
Share
</Button>
</>
) : null}
<ThemeSwitcher />
<UserButton userData={userData} />
</div>
</div>
</>
);
)
}

View File

@ -32,7 +32,27 @@ export default function EditorTerminal({
const terminal = new Terminal({
cursorBlink: true,
theme: {
background: "#262626",
foreground: "#2e3436",
background: "#ffffff",
black: "#2e3436",
brightBlack: "#555753",
red: "#cc0000",
brightRed: "#ef2929",
green: "#4e9a06",
brightGreen: "#8ae234",
yellow: "#c4a000",
brightYellow: "#fce94f",
blue: "#3465a4",
brightBlue: "#729fcf",
magenta: "#75507b",
brightMagenta: "#ad7fa8",
cyan: "#06989a",
brightCyan: "#34e2e2",
white: "#d3d7cf",
brightWhite: "#eeeeec",
cursor: "#2e3436",
cursorAccent: "#ffffff",
selection: "rgba(52, 101, 164, 0.3)",
},
fontFamily: "var(--font-geist-mono)",
fontSize: 14,

View File

@ -1,9 +1,10 @@
import Image from "next/image"
import Logo from "@/assets/logo.svg"
import XLogo from "@/assets/x.svg"
import Button from "@/components/ui/customButton"
import CustomButton from "@/components/ui/customButton"
import { ChevronRight } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { Button } from "../ui/button"
import { ThemeSwitcher } from "../ui/theme-switcher"
export default function Landing() {
return (
@ -20,21 +21,37 @@ export default function Landing() {
/>
</div>
<div className="flex items-center space-x-4">
<Button variant="outline" size="icon" asChild>
<a href="https://www.x.com/ishaandey_" target="_blank">
<Image src={XLogo} alt="X Logo" width={18} height={18} />
<svg
width="1200"
height="1227"
viewBox="0 0 1200 1227"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="size-[1.125rem] text-muted-foreground"
>
<path
d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z"
fill="currentColor"
/>
</svg>
</a>
</Button>
<ThemeSwitcher />
</div>
</div>
<h1 className="text-2xl font-medium text-center mt-16">
A Collaborative + AI-Powered Code Environment
</h1>
<div 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
custom AI code autocompletion and real-time collaboration.
</div>
</p>
<div className="mt-8 flex space-x-4">
<Link href="/sign-up">
<Button>Go To App</Button>
<CustomButton>Go To App</CustomButton>
</Link>
<a
href="https://github.com/ishaan1013/sandbox"

View File

@ -1,6 +1,5 @@
import * as React from "react"
import { Plus } from "lucide-react"
import { cn } from "@/lib/utils"
import * as React from "react"
const Button = ({
children,
@ -25,7 +24,7 @@ const Button = ({
`gradient-button-bg p-[1px] inline-flex group rounded-md text-sm font-medium focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50`
)}
>
<div className="rounded-[6px] w-full gradient-button flex items-center justify-center whitespace-nowrap px-4 py-2 h-9">
<div className="rounded-[6px] w-full gradient-button transition-colors flex items-center justify-center whitespace-nowrap px-4 py-2 h-9">
{children}
</div>
</button>

View File

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

View File

@ -0,0 +1,39 @@
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ThemeSwitcher() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<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" />
<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>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}