Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ docker-compose up

Access the app at http://localhost:80

By default the backend mounts `./default-files` and exposes its contents through the editor. If you want to serve a different local project, edit the `server` volume mapping in `docker-compose.yml` (replace `./default-files` with your desired path) before running `docker-compose up`.

For detailed setup instructions, development guidelines, and contribution information, see [CONTRIBUTING.md](CONTRIBUTING.md).

## On the Horizon
Expand Down
4 changes: 2 additions & 2 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 93 additions & 2 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
TerminalHandle,
SearchOptions,
MatchInfo,
EditorLanguageKey,
} from "./types/editor";

import {
Expand All @@ -25,7 +26,6 @@ import {
DEFAULT_WEBVIEW_WIDTH_FRACTION,
MIN_WEBVIEW_WIDTH,
} from "./constants/layout";
import { MOCK_FILES } from "./constants/mockFiles";
import { isExecutableLanguage } from "./utils/languageUtils";
import { useResizablePanel } from "./hooks/useResizablePanel";
import { useCollaborationSession } from "./hooks/useCollaborationSession";
Expand All @@ -35,6 +35,7 @@ import Sidebar from "./components/Sidebar";
import MainEditorArea from "./components/MainEditorArea";
import { useFileStore } from "./store/useFileStore";
import { Analytics } from "@vercel/analytics/react";
import type { CatalogFile } from "./types/editor";

// Interface for Monaco's find controller
interface FindControllerInterface extends editor.IEditorContribution {
Expand All @@ -57,6 +58,26 @@ interface FindControllerInterface extends editor.IEditorContribution {
closeFindWidget(): void;
}

interface DefaultFileResponse {
id?: string;
name: string;
language?: string;
content: string;
}

const inferLanguageFromFileName = (fileName: string): EditorLanguageKey => {
const normalized = fileName.toLowerCase();
if (normalized.endsWith(".html")) return "html";
if (normalized.endsWith(".css")) return "css";
if (normalized.endsWith(".tsx")) return "typescript";
if (normalized.endsWith(".ts")) return "typescript";
if (normalized.endsWith(".jsx")) return "javascript";
if (normalized.endsWith(".js")) return "javascript";
if (normalized.endsWith(".json")) return "json";
if (normalized.endsWith(".md")) return "markdown";
return "plaintext";
};

const App = () => {
// REFS
const terminalRef = useRef<TerminalHandle | null>(null);
Expand All @@ -65,18 +86,23 @@ const App = () => {
const mainContentRef = useRef<HTMLDivElement>(null);
const tabContainerRef = useRef<HTMLDivElement>(null);
const editorInstanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const defaultFilesFetchAttempted = useRef(false);

// STATE
const [activeIcon, setActiveIcon] = useState<string | null>(null);

const { openFiles, activeFileId } = useFileStore();

const fileContents = useFileStore((state) => state.fileContents);
const filesCatalog = useFileStore((state) => state.filesCatalog);

const openFile = useFileStore((state) => state.openFile);
const closeFile = useFileStore((state) => state.closeFile);
const switchTab = useFileStore((state) => state.switchTab);
const setFileContent = useFileStore((state) => state.setFileContent);
const initializeFromCatalog = useFileStore(
(state) => state.initializeFromCatalog
);

const [isViewMenuOpen, setIsViewMenuOpen] = useState(false);

Expand Down Expand Up @@ -607,6 +633,71 @@ const App = () => {
return Array.from(uniqueUsersMap.values());
}, [remoteUsers, userId]);

useEffect(() => {
if (defaultFilesFetchAttempted.current) {
return;
}

defaultFilesFetchAttempted.current = true;

const loadDefaultFilesFromRepository = async () => {
try {
const response = await axios.get<DefaultFileResponse[]>(
`${import.meta.env.VITE_BACKEND_URL}/api/files/defaults`
);
const repositoryFiles = response.data;

if (Array.isArray(repositoryFiles) && repositoryFiles.length > 0) {
const catalog = repositoryFiles.reduce(
(acc, file) => {
const fileId = file.id ?? file.name;
if (!fileId || !file.name || typeof file.content !== "string") {
return acc;
}

const normalizedLanguage =
(file.language as EditorLanguageKey | undefined) ??
inferLanguageFromFileName(fileId);

acc[fileId] = {
name: file.name,
language: normalizedLanguage,
content: file.content,
};

return acc;
},
{} as Record<string, CatalogFile>
);

if (Object.keys(catalog).length > 0) {
const preferredIds = [
"index.html",
"style.css",
"script.js",
].filter((id) => catalog[id] !== undefined);

const fallbackOrder = repositoryFiles
.map((file) => file.id ?? file.name)
.filter((id): id is string => !!id && catalog[id] !== undefined);

initializeFromCatalog(
catalog,
preferredIds.length > 0 ? preferredIds : fallbackOrder
);
}
}
} catch (error) {
console.error(
"Failed to load default files from local repository:",
error
);
}
};

loadDefaultFilesFromRepository();
}, [initializeFromCatalog]);

// Handle sending chat messages
const handleSendChatMessage = useCallback(
(message: string) => {
Expand Down Expand Up @@ -775,7 +866,7 @@ const App = () => {
activeFileId={activeFileId}
isSessionActive={isSessionActive}
handleOpenFile={(fileId) => openFile(fileId, isSessionActive)}
mockFiles={MOCK_FILES}
fileCatalog={filesCatalog}
onSearchChange={handleSearchChange}
onReplaceChange={handleReplaceChange}
onToggleSearchOption={handleToggleSearchOption}
Expand Down
74 changes: 40 additions & 34 deletions client/src/components/FileExplorerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,18 @@ import {
languageColorMap,
defaultIconColor,
} from "../constants/mappings";
import { EditorLanguageKey } from "../types/editor";
import { MockFile } from "../constants/mockFiles";
import { CatalogFile, EditorLanguageKey } from "../types/editor";

// Props for FileExplorerPanel - initially minimal, might adjust
interface FileExplorerPanelProps {
isSessionActive: boolean;
handleOpenFile: (fileId: string) => void;
mockFiles: { [key: string]: MockFile };
fileCatalog: { [key: string]: CatalogFile };
activeFileId: string | null;
}

const FileExplorerPanel = ({
handleOpenFile,
mockFiles,
fileCatalog,
activeFileId,
}: FileExplorerPanelProps) => {
const [isProjectExpanded, setIsProjectExpanded] = useState(true);
Expand Down Expand Up @@ -62,37 +60,45 @@ const FileExplorerPanel = ({
<div className="relative">
<div className="absolute top-0 bottom-0 left-[12px] w-px bg-stone-600/50 z-0"></div>

{Object.entries(mockFiles).map(([id, file]) => {
const IconComponent =
languageIconMap[file.language as EditorLanguageKey] || VscFile;
const iconColor =
languageColorMap[file.language as EditorLanguageKey] ||
defaultIconColor;
return (
<div
key={id}
className={`relative flex items-center text-sm py-1 cursor-pointer w-full pl-4 z-10 ${
activeFileId === id
? "bg-stone-600/50 shadow-[inset_0_1px_0_#78716c,inset_0_-1px_0_#78716c] hover:bg-stone-600/50"
: "hover:bg-stone-700/50"
}`}
onClick={() => handleOpenFile(id)}
title={file.name}
>
<IconComponent
size={18}
className={`mr-1 flex-shrink-0 ${iconColor}`}
/>
<span
className={`w-full truncate ${
activeFileId === id ? "text-stone-100" : "text-stone-400"
{Object.keys(fileCatalog).length === 0 ? (
<div className="pl-4 pr-2 py-3 text-sm text-stone-500">
No files loaded yet.
</div>
) : (
Object.entries(fileCatalog).map(([id, file]) => {
const IconComponent =
languageIconMap[file.language as EditorLanguageKey] || VscFile;
const iconColor =
languageColorMap[file.language as EditorLanguageKey] ||
defaultIconColor;
return (
<div
key={id}
className={`relative flex items-center text-sm py-1 cursor-pointer w-full pl-4 z-10 ${
activeFileId === id
? "bg-stone-600/50 shadow-[inset_0_1px_0_#78716c,inset_0_-1px_0_#78716c] hover:bg-stone-600/50"
: "hover:bg-stone-700/50"
}`}
onClick={() => handleOpenFile(id)}
title={file.name}
>
{file.name}
</span>
</div>
);
})}
<IconComponent
size={18}
className={`mr-1 flex-shrink-0 ${iconColor}`}
/>
<span
className={`w-full truncate ${
activeFileId === id
? "text-stone-100"
: "text-stone-400"
}`}
>
{file.name}
</span>
</div>
);
})
)}
</div>
)}
</div>
Expand Down
15 changes: 9 additions & 6 deletions client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import SearchPanel from "./SearchPanel";
import SessionParticipantsPanel from "./SessionParticipantsPanel";
import FileExplorerPanel from "./FileExplorerPanel";
import { RemoteUser, ChatMessageType } from "../types/props";
import { JoinStateType, SearchOptions, MatchInfo } from "../types/editor";
import { MockFile } from "../constants/mockFiles";
import {
JoinStateType,
SearchOptions,
MatchInfo,
CatalogFile,
} from "../types/editor";
import { ICON_BAR_WIDTH, EXPLORER_HANDLE_WIDTH } from "../constants/layout";
import { COLORS } from "../constants/colors";

Expand All @@ -39,7 +43,7 @@ interface SidebarProps {
isSessionActive: boolean;
activeFileId: string | null;
handleOpenFile: (fileId: string) => void;
mockFiles: { [key: string]: MockFile };
fileCatalog: { [key: string]: CatalogFile };
onSearchChange: (term: string, options: SearchOptions) => void;
onReplaceChange: (value: string) => void;
onToggleSearchOption: (optionKey: keyof SearchOptions) => void;
Expand Down Expand Up @@ -77,7 +81,7 @@ const Sidebar = ({
isSessionActive,
activeFileId,
handleOpenFile,
mockFiles,
fileCatalog,
onSearchChange,
onReplaceChange,
onToggleSearchOption,
Expand Down Expand Up @@ -207,9 +211,8 @@ const Sidebar = ({
}`}
>
<FileExplorerPanel
isSessionActive={isSessionActive}
handleOpenFile={handleOpenFile}
mockFiles={mockFiles}
fileCatalog={fileCatalog}
activeFileId={activeFileId}
/>
</div>
Expand Down
Loading