mirror of
https://github.com/khairul169/code-share.git
synced 2026-10-24 11:53:58 +07:00
feat: rebuild app using vike
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Console, Decode } from "console-feed";
|
||||
import type { Message } from "console-feed/lib/definitions/Console";
|
||||
import ErrorBoundary from "~/components/containers/error-boundary";
|
||||
|
||||
const ConsoleLogger = () => {
|
||||
const [logs, setLogs] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMessage = (event: MessageEvent<any>) => {
|
||||
const { data: eventData } = event;
|
||||
if (!eventData || eventData.type !== "console") {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = Decode(eventData.data);
|
||||
if (!data || !data.method || !data.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLogs((i) => [data, ...i]);
|
||||
};
|
||||
|
||||
window.addEventListener("message", onMessage);
|
||||
return () => {
|
||||
window.removeEventListener("message", onMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-[#242424] border-t border-t-gray-600">
|
||||
<p className="py-2 px-3 uppercase text-xs">Console</p>
|
||||
<ErrorBoundary>
|
||||
<div className="overflow-y-auto flex-1">
|
||||
<Console logs={logs} variant="dark" />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsoleLogger;
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../../../../components/ui/dialog";
|
||||
import { UseDiscloseReturn } from "~/hooks/useDisclose";
|
||||
import { Input } from "../../../../components/ui/input";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { useForm } from "~/hooks/useForm";
|
||||
import { z } from "zod";
|
||||
import FormErrorMessage from "../../../../components/ui/form-error-message";
|
||||
import trpc from "~/lib/trpc";
|
||||
import type { FileSchema } from "~/server/db/schema/file";
|
||||
import { useWatch } from "react-hook-form";
|
||||
|
||||
type Props = {
|
||||
disclose: UseDiscloseReturn<CreateFileSchema>;
|
||||
onSuccess?: (file: FileSchema, type: "create" | "update") => void;
|
||||
};
|
||||
|
||||
const fileSchema = z.object({
|
||||
id: z.number().optional(),
|
||||
parentId: z.number().optional().nullable(),
|
||||
filename: z.string().min(1),
|
||||
isDirectory: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type CreateFileSchema = z.infer<typeof fileSchema>;
|
||||
|
||||
const defaultValues: z.infer<typeof fileSchema> = {
|
||||
filename: "",
|
||||
};
|
||||
|
||||
const CreateFileDialog = ({ disclose, onSuccess }: Props) => {
|
||||
const form = useForm(fileSchema, disclose.data || defaultValues);
|
||||
const isDir = useWatch({ name: "isDirectory", control: form.control });
|
||||
|
||||
const create = trpc.file.create.useMutation({
|
||||
onSuccess(data) {
|
||||
if (onSuccess) onSuccess(data, "create");
|
||||
disclose.onClose();
|
||||
},
|
||||
});
|
||||
const update = trpc.file.update.useMutation({
|
||||
onSuccess(data) {
|
||||
if (onSuccess) onSuccess(data, "update");
|
||||
disclose.onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit((values) => {
|
||||
if (update.isPending || create.isPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.id) {
|
||||
update.mutate({ id: values.id!, ...values });
|
||||
} else {
|
||||
create.mutate(values);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={disclose.isOpen} onOpenChange={disclose.onChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{[
|
||||
!disclose.data?.id ? "Create" : "Rename",
|
||||
isDir ? "Directory" : "File",
|
||||
].join(" ")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={onSubmit}>
|
||||
<FormErrorMessage form={form} />
|
||||
|
||||
<Input
|
||||
placeholder={isDir ? "Directory Name" : "Filename"}
|
||||
autoFocus
|
||||
{...form.register("filename")}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-4 mt-4">
|
||||
<Button size="sm" variant="ghost" onClick={disclose.onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" size="sm">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateFileDialog;
|
||||
@@ -0,0 +1,229 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "~/components/ui/resizable";
|
||||
import Tabs, { Tab } from "~/components/ui/tabs";
|
||||
import FileViewer from "./file-viewer";
|
||||
import trpc from "~/lib/trpc";
|
||||
import EditorContext from "../context/editor";
|
||||
import type { FileSchema } from "~/server/db/schema/file";
|
||||
import { usePortrait } from "~/hooks/usePortrait";
|
||||
import Panel from "~/components/ui/panel";
|
||||
import { previewStore } from "../stores/web-preview";
|
||||
import { useProjectContext } from "../context/project";
|
||||
import { ImperativePanelHandle } from "react-resizable-panels";
|
||||
import Sidebar from "./sidebar";
|
||||
import useCommandKey from "~/hooks/useCommandKey";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { FaCompress, FaCompressArrowsAlt } from "react-icons/fa";
|
||||
import ConsoleLogger from "./console-logger";
|
||||
|
||||
const Editor = () => {
|
||||
const isPortrait = usePortrait();
|
||||
const trpcUtils = trpc.useUtils();
|
||||
const [isMounted, setMounted] = useState(false);
|
||||
const project = useProjectContext();
|
||||
const sidebarPanel = useRef<ImperativePanelHandle>(null);
|
||||
|
||||
const [sidebarExpanded, setSidebarExpanded] = useState(false);
|
||||
const [curTabIdx, setCurTabIdx] = useState(0);
|
||||
const [curOpenFiles, setOpenFiles] = useState<number[]>([]);
|
||||
|
||||
const pinnedFiles = trpc.file.getAll.useQuery(
|
||||
{ isPinned: true },
|
||||
{ enabled: !isMounted }
|
||||
);
|
||||
const openedFilesData = trpc.file.getAll.useQuery(
|
||||
{ id: curOpenFiles },
|
||||
{ enabled: curOpenFiles.length > 0 }
|
||||
);
|
||||
const [openedFiles, setOpenedFiles] = useState<any[]>([]);
|
||||
|
||||
const deleteFile = trpc.file.delete.useMutation({
|
||||
onSuccess: (file) => {
|
||||
trpcUtils.file.getAll.invalidate();
|
||||
onFileChanged(file);
|
||||
|
||||
const openFileIdx = curOpenFiles.indexOf(file.id);
|
||||
if (openFileIdx >= 0) {
|
||||
onCloseFile(openFileIdx);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const toggleSidebar = useCallback(() => {
|
||||
const sidebar = sidebarPanel.current;
|
||||
if (!sidebar) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sidebar.isExpanded()) {
|
||||
sidebar.collapse();
|
||||
} else {
|
||||
sidebar.expand();
|
||||
sidebar.resize(25);
|
||||
}
|
||||
}, [sidebarPanel]);
|
||||
|
||||
useCommandKey("b", toggleSidebar);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pinnedFiles.data?.length || curOpenFiles.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pinnedFiles.data.forEach((file) => {
|
||||
onOpenFile(file.id, false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pinnedFiles.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openedFilesData.data) {
|
||||
setOpenedFiles(openedFilesData.data);
|
||||
}
|
||||
}, [openedFilesData.data]);
|
||||
|
||||
const onOpenFile = useCallback(
|
||||
(fileId: number, autoSwitchTab = true) => {
|
||||
const idx = curOpenFiles.indexOf(fileId);
|
||||
if (idx >= 0) {
|
||||
return setCurTabIdx(idx);
|
||||
}
|
||||
|
||||
setOpenFiles((state) => {
|
||||
if (autoSwitchTab) {
|
||||
setCurTabIdx(state.length);
|
||||
}
|
||||
return [...state, fileId];
|
||||
});
|
||||
},
|
||||
[curOpenFiles]
|
||||
);
|
||||
|
||||
const onDeleteFile = useCallback(
|
||||
(fileId: number) => {
|
||||
if (
|
||||
window.confirm("Are you sure want to delete this files?") &&
|
||||
!deleteFile.isPending
|
||||
) {
|
||||
deleteFile.mutate(fileId);
|
||||
}
|
||||
},
|
||||
[deleteFile]
|
||||
);
|
||||
|
||||
const onCloseFile = useCallback(
|
||||
(idx: number) => {
|
||||
const _f = [...curOpenFiles];
|
||||
_f.splice(idx, 1);
|
||||
setOpenFiles(_f);
|
||||
|
||||
if (curTabIdx === idx) {
|
||||
setCurTabIdx(Math.max(0, idx - 1));
|
||||
}
|
||||
},
|
||||
[curOpenFiles, curTabIdx]
|
||||
);
|
||||
|
||||
const onFileChanged = useCallback(
|
||||
(_file: Omit<FileSchema, "content">) => {
|
||||
openedFilesData.refetch();
|
||||
},
|
||||
[openedFilesData]
|
||||
);
|
||||
|
||||
const refreshPreview = useCallback(() => {
|
||||
previewStore.getState().refresh();
|
||||
}, []);
|
||||
|
||||
const openFileList = useMemo(() => {
|
||||
return curOpenFiles.map((fileId) => {
|
||||
const fileData = openedFiles?.find((i) => i.id === fileId);
|
||||
|
||||
return {
|
||||
title: fileData?.filename || "...",
|
||||
render: () => (
|
||||
<FileViewer id={fileId} onFileContentChange={refreshPreview} />
|
||||
),
|
||||
};
|
||||
}) satisfies Tab[];
|
||||
}, [curOpenFiles, openedFiles, refreshPreview]);
|
||||
|
||||
const PanelComponent = !project.isCompact ? Panel : "div";
|
||||
|
||||
return (
|
||||
<EditorContext.Provider
|
||||
value={{
|
||||
onOpenFile,
|
||||
onFileChanged,
|
||||
onDeleteFile,
|
||||
}}
|
||||
>
|
||||
<PanelComponent className="h-full relative">
|
||||
<ResizablePanelGroup autoSaveId="veditor-panel" direction="horizontal">
|
||||
<ResizablePanel
|
||||
ref={sidebarPanel}
|
||||
defaultSize={isPortrait ? 0 : 25}
|
||||
minSize={10}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
className="bg-[#1e2536]"
|
||||
onExpand={() => setSidebarExpanded(true)}
|
||||
onCollapse={() => setSidebarExpanded(false)}
|
||||
>
|
||||
<Sidebar />
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle className="bg-slate-900" />
|
||||
|
||||
<ResizablePanel defaultSize={isPortrait ? 100 : 75}>
|
||||
<ResizablePanelGroup
|
||||
autoCapitalize="code-editor"
|
||||
direction="vertical"
|
||||
>
|
||||
<ResizablePanel defaultSize={isPortrait ? 100 : 80} minSize={20}>
|
||||
<Tabs
|
||||
tabs={openFileList}
|
||||
current={curTabIdx}
|
||||
onChange={setCurTabIdx}
|
||||
onClose={onCloseFile}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
|
||||
{!isPortrait ? (
|
||||
<>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
defaultSize={20}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
minSize={10}
|
||||
>
|
||||
<ConsoleLogger />
|
||||
</ResizablePanel>
|
||||
</>
|
||||
) : null}
|
||||
</ResizablePanelGroup>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute bottom-0 left-0 w-12 h-12 rounded-none flex items-center justify-center"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
{sidebarExpanded ? <FaCompressArrowsAlt /> : <FaCompress />}
|
||||
</Button>
|
||||
</PanelComponent>
|
||||
</EditorContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
@@ -0,0 +1,239 @@
|
||||
"use client";
|
||||
|
||||
import React, { Fragment, useMemo, useState } from "react";
|
||||
import { UseDiscloseReturn, useDisclose } from "~/hooks/useDisclose";
|
||||
import {
|
||||
FiChevronRight,
|
||||
FiFilePlus,
|
||||
FiFolderPlus,
|
||||
FiMoreVertical,
|
||||
} from "react-icons/fi";
|
||||
import { FaCheck, FaThumbtack } from "react-icons/fa";
|
||||
import trpc from "~/lib/trpc";
|
||||
import type { FileSchema } from "~/server/db/schema/file";
|
||||
import CreateFileDialog, { CreateFileSchema } from "./createfile-dialog";
|
||||
import ActionButton from "../../../../components/ui/action-button";
|
||||
import { useEditorContext } from "../context/editor";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import { cn, getUrl } from "~/lib/utils";
|
||||
import FileIcon from "~/components/ui/file-icon";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { useParams } from "~/renderer/hooks";
|
||||
|
||||
const FileListing = () => {
|
||||
const { onOpenFile, onFileChanged } = useEditorContext();
|
||||
const createFileDlg = useDisclose<CreateFileSchema>();
|
||||
const files = trpc.file.getAll.useQuery();
|
||||
|
||||
const fileList = useMemo(() => groupFiles(files.data, null), [files.data]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="h-10 flex items-center pl-4 pr-1">
|
||||
<p className="text-xs uppercase truncate flex-1">My Project</p>
|
||||
<ActionButton
|
||||
icon={FiFilePlus}
|
||||
onClick={() => createFileDlg.onOpen()}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
icon={FiFolderPlus}
|
||||
onClick={() =>
|
||||
createFileDlg.onOpen({ isDirectory: true, filename: "" })
|
||||
}
|
||||
/>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ActionButton icon={FiMoreVertical} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem>Upload File</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Project Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Download Project</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-stretch flex-1 overflow-y-auto">
|
||||
{fileList.map((file) => (
|
||||
<FileItem key={file.id} file={file} createFileDlg={createFileDlg} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<CreateFileDialog
|
||||
disclose={createFileDlg}
|
||||
onSuccess={(file, type) => {
|
||||
files.refetch();
|
||||
|
||||
if (type === "create" && !file.isDirectory && !file.isFile) {
|
||||
onOpenFile && onOpenFile(file.id);
|
||||
}
|
||||
if (onFileChanged) {
|
||||
onFileChanged(file);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
type TFile = Omit<FileSchema, "content"> & { children: TFile[] };
|
||||
|
||||
type FileItemProps = {
|
||||
file: TFile;
|
||||
createFileDlg: UseDiscloseReturn<CreateFileSchema>;
|
||||
};
|
||||
|
||||
const FileItem = ({ file, createFileDlg }: FileItemProps) => {
|
||||
const { slug } = useParams();
|
||||
const { onOpenFile, onDeleteFile } = useEditorContext();
|
||||
const [isCollapsed, setCollapsed] = useState(false);
|
||||
const trpcUtils = trpc.useUtils();
|
||||
|
||||
const updateFile = trpc.file.update.useMutation({
|
||||
onSuccess() {
|
||||
trpcUtils.file.getAll.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="group text-slate-400 hover:text-white hover:bg-slate-700 transition-colors text-sm flex items-stretch relative w-full h-10">
|
||||
<button
|
||||
className="flex items-center pl-5 pr-3 gap-1 w-full text-left"
|
||||
onClick={() => {
|
||||
if (file.isDirectory) {
|
||||
setCollapsed((i) => !i);
|
||||
} else {
|
||||
onOpenFile(file.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{file.isDirectory ? (
|
||||
<FiChevronRight
|
||||
className={cn(
|
||||
"absolute left-1 top-3 transition-transform",
|
||||
isCollapsed ? "rotate-90" : ""
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<FileIcon file={file} />
|
||||
<span className="flex-1 truncate">{file.filename}</span>
|
||||
|
||||
{file.isPinned ? <FaThumbtack /> : null}
|
||||
</button>
|
||||
|
||||
<div className="flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity absolute top-0 right-0 pr-1 h-full bg-slate-700">
|
||||
{file.isDirectory ? (
|
||||
<>
|
||||
<ActionButton
|
||||
icon={FiFilePlus}
|
||||
onClick={() =>
|
||||
createFileDlg.onOpen({ parentId: file.id, filename: "" })
|
||||
}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
icon={FiFolderPlus}
|
||||
onClick={() =>
|
||||
createFileDlg.onOpen({
|
||||
parentId: file.id,
|
||||
isDirectory: true,
|
||||
filename: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ActionButton icon={FiMoreVertical} />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => createFileDlg.onOpen(file)}>
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
{/* <DropdownMenuItem>Duplicate</DropdownMenuItem> */}
|
||||
<DropdownMenuItem onClick={() => onDeleteFile(file.id)}>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => copy(file.filename)}>
|
||||
Copy Name
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => copy(file.path)}>
|
||||
Copy Path
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => copy(getUrl(`api/preview/${slug}`, file.path))}
|
||||
>
|
||||
Copy URL
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
window.open(
|
||||
getUrl(`api/preview/${slug}`, file.path),
|
||||
"_blank"
|
||||
)
|
||||
}
|
||||
>
|
||||
Open in new tab
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
return updateFile.mutate({
|
||||
id: file.id,
|
||||
isPinned: !file.isPinned,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Pinned
|
||||
{file.isPinned ? <FaCheck className="ml-auto" /> : null}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCollapsed && file.children?.length > 0 ? (
|
||||
<div className="flex flex-col items-stretch pl-4">
|
||||
{file.children.map((file) => (
|
||||
<FileItem key={file.id} file={file} createFileDlg={createFileDlg} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function groupFiles(files?: any[] | null, parentId?: number | null) {
|
||||
if (!files) {
|
||||
return [];
|
||||
}
|
||||
const groupedFiles: TFile[] = [];
|
||||
|
||||
files.forEach((file) => {
|
||||
if (file.parentId !== parentId) {
|
||||
return;
|
||||
}
|
||||
groupedFiles.push(file);
|
||||
if (file.isDirectory) {
|
||||
file.children = groupFiles(files, file.id);
|
||||
}
|
||||
});
|
||||
|
||||
return groupedFiles;
|
||||
}
|
||||
|
||||
export default FileListing;
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { getFileExt } from "~/lib/utils";
|
||||
import React from "react";
|
||||
import CodeEditor from "../../../../components/ui/code-editor";
|
||||
import trpc from "~/lib/trpc";
|
||||
|
||||
type Props = {
|
||||
id: number;
|
||||
onFileContentChange?: () => void;
|
||||
};
|
||||
|
||||
const FileViewer = ({ id, onFileContentChange }: Props) => {
|
||||
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id);
|
||||
const updateFileContent = trpc.file.update.useMutation({
|
||||
onSuccess: () => {
|
||||
if (onFileContentChange) onFileContentChange();
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
if (!data || data.isDirectory) {
|
||||
return <p>File not found.</p>;
|
||||
}
|
||||
|
||||
const { filename } = data;
|
||||
|
||||
if (!data.isFile) {
|
||||
const ext = getFileExt(filename);
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
lang={ext}
|
||||
value={data?.content || ""}
|
||||
formatOnSave
|
||||
onChange={(val) => updateFileContent.mutate({ id, content: val })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default FileViewer;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import FileListing from "./file-listing";
|
||||
import { FaUserCircle } from "react-icons/fa";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<aside className="flex flex-col items-stretch h-full">
|
||||
<FileListing />
|
||||
<div className="h-12 bg-[#1a1b26] pl-12">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-12 w-full truncate flex justify-start text-left uppercase text-xs rounded-none"
|
||||
>
|
||||
<FaUserCircle className="mr-2 text-xl" />
|
||||
<span className="truncate">Log in</span>
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,67 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import Panel from "~/components/ui/panel";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useProjectContext } from "../context/project";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { FaEllipsisV, FaRedo } from "react-icons/fa";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { previewStore } from "../stores/web-preview";
|
||||
|
||||
type WebPreviewProps = {
|
||||
url?: string | null;
|
||||
};
|
||||
|
||||
const WebPreview = ({ url }: WebPreviewProps) => {
|
||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||
const project = useProjectContext();
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
if (frameRef.current) {
|
||||
frameRef.current.src = `${url}?t=${Date.now()}`;
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
useEffect(() => {
|
||||
previewStore.setState({ refresh });
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const PanelComponent = !project.isCompact ? Panel : "div";
|
||||
|
||||
return (
|
||||
<PanelComponent className="h-full flex flex-col bg-slate-800">
|
||||
<div className="h-10 flex items-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="dark:hover:bg-slate-700"
|
||||
onClick={refresh}
|
||||
>
|
||||
<FaRedo />
|
||||
</Button>
|
||||
<Input
|
||||
className="flex-1 dark:bg-gray-900 dark:hover:bg-gray-950 h-8 rounded-full"
|
||||
value={url || ""}
|
||||
readOnly
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="dark:hover:bg-slate-700"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<FaEllipsisV />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{url != null ? (
|
||||
<iframe
|
||||
id="web-preview"
|
||||
ref={frameRef}
|
||||
className="border-none w-full flex-1 overflow-hidden bg-white"
|
||||
sandbox="allow-scripts"
|
||||
/>
|
||||
) : null}
|
||||
</PanelComponent>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebPreview;
|
||||
Reference in New Issue
Block a user