feat: add personal project list, add project settings, etc

This commit is contained in:
2024-02-24 03:55:50 +00:00
parent 49b80a2f4c
commit 751210c819
31 changed files with 877 additions and 148 deletions
+8 -1
View File
@@ -18,6 +18,8 @@ import { Data } from "../+data";
import { useBreakpoint } from "~/hooks/useBreakpoint";
import StatusBar from "./status-bar";
import { FiTerminal } from "react-icons/fi";
import SettingsDialog from "./settings-dialog";
import FileIcon from "~/components/ui/file-icon";
const Editor = () => {
const { project, initialFiles } = useData<Data>();
@@ -125,9 +127,11 @@ const Editor = () => {
tabs = tabs.concat(
curOpenFiles.map((fileId) => {
const fileData = openedFiles?.find((i) => i.id === fileId);
const filename = fileData?.filename || "...";
return {
title: fileData?.filename || "...",
title: filename,
icon: <FileIcon file={{ isDirectory: false, filename }} />,
render: () => <FileViewer id={fileId} />,
};
})
@@ -180,6 +184,7 @@ const Editor = () => {
current={Math.min(Math.max(curTabIdx, 0), tabs.length - 1)}
onChange={setCurTabIdx}
onClose={onCloseFile}
className="h-full"
/>
</ResizablePanel>
@@ -203,6 +208,8 @@ const Editor = () => {
<StatusBar className="order-1 md:order-2" />
</PanelComponent>
<SettingsDialog />
</EditorContext.Provider>
);
};
@@ -24,6 +24,7 @@ import FileIcon from "~/components/ui/file-icon";
import { useData } from "~/renderer/hooks";
import Spinner from "~/components/ui/spinner";
import { Data } from "../+data";
import { settingsDialog } from "../stores/dialogs";
const FileListing = () => {
const { project, files: initialFiles } = useData<Data>();
@@ -59,7 +60,9 @@ const FileListing = () => {
<DropdownMenuContent align="start">
<DropdownMenuItem>Upload File</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Project Settings</DropdownMenuItem>
<DropdownMenuItem onClick={() => settingsDialog.setState(true)}>
Project Settings
</DropdownMenuItem>
<DropdownMenuItem>Download Project</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -184,13 +187,18 @@ const FileItem = ({ file, createFileDlg }: FileItemProps) => {
Copy Path
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => copy(getPreviewUrl(project, file))}
onClick={() =>
copy(getPreviewUrl(project, file, { raw: true }))
}
>
Copy URL
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
window.open(getPreviewUrl(project, file), "_blank")
window.open(
getPreviewUrl(project, file, { raw: true }),
"_blank"
)
}
>
Open in new tab
@@ -0,0 +1,175 @@
import { useStore } from "zustand";
import { settingsDialog } from "../stores/dialogs";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { useMemo, useState } from "react";
import { useProjectContext } from "../context/project";
import { useForm, useFormReturn } from "~/hooks/useForm";
import Input from "~/components/ui/input";
import Select from "~/components/ui/select";
import { Button } from "~/components/ui/button";
import { ProjectSettingsSchema, projectSettingsSchema } from "../lib/schema";
import {
cssPreprocessorList,
jsTranspilerList,
visibilityList,
} from "../lib/consts";
import trpc from "~/lib/trpc";
import { toast } from "~/lib/utils";
import Checkbox from "~/components/ui/checkbox";
import Tabs, { Tab } from "~/components/ui/tabs";
import { navigate } from "vike/client/router";
const defaultValues: ProjectSettingsSchema = {
title: "",
// slug: "",
visibility: "private",
settings: {
css: {
preprocessor: null,
tailwindcss: false,
},
js: {
transpiler: null,
packages: [],
},
},
};
const SettingsDialog = () => {
const { project } = useProjectContext();
const [tab, setTab] = useState(0);
const initialValues = useMemo(() => {
return Object.assign(defaultValues, {
title: project.title,
// slug: project.slug,
settings: project.settings,
});
}, [project]);
const open = useStore(settingsDialog);
const form = useForm(projectSettingsSchema, initialValues);
const save = trpc.project.update.useMutation({
onSuccess(data) {
toast.success("Project updated!");
onClose();
if (data.slug !== project.slug) {
navigate(`/${data.slug}`);
}
},
});
const onClose = () => {
settingsDialog.setState(false);
};
const onSubmit = form.handleSubmit((values) => {
save.mutate({ ...values, id: project.id });
});
const tabs: Tab[] = useMemo(
() => [
{
title: "General",
render: () => <GeneralTab form={form} />,
},
{
title: "CSS",
render: () => <CSSTab form={form} />,
},
{
title: "Javascript",
render: () => <JSTab form={form} />,
},
],
[]
);
return (
<Dialog open={open} onOpenChange={settingsDialog.setState}>
<DialogContent>
<DialogHeader>
<DialogTitle>Project Settings</DialogTitle>
</DialogHeader>
<form onSubmit={onSubmit} method="post">
<Tabs
tabs={tabs}
current={tab}
onChange={setTab}
containerClassName="mt-4"
/>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center sm:justify-end gap-4 mt-8">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" isLoading={save.isPending}>
Save Settings
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
};
type TabProps = {
form: useFormReturn<ProjectSettingsSchema>;
};
const GeneralTab = ({ form }: TabProps) => {
return (
<div className="space-y-3">
<Input form={form} name="title" label="Title" />
{/* <Input form={form} name="slug" label="Slug" /> */}
<Select
form={form}
name="visibility"
label="Visibility"
items={visibilityList}
/>
</div>
);
};
const CSSTab = ({ form }: TabProps) => {
return (
<div className="space-y-3">
<Select
form={form}
name="settings.css.preprocessor"
label="Preprocessor"
items={cssPreprocessorList}
/>
<Checkbox
form={form}
name="settings.css.tailwindcss"
label="Tailwindcss"
/>
</div>
);
};
const JSTab = ({ form }: TabProps) => {
return (
<div className="space-y-3">
<Select
form={form}
name="settings.js.transpiler"
label="Transpiler"
items={jsTranspilerList}
/>
<p className="text-sm">
* Set transpiler to <strong>SWC</strong> to use JSX
</p>
</div>
);
};
export default SettingsDialog;
+31 -17
View File
@@ -1,5 +1,11 @@
import React from "react";
import { FiArrowLeft, FiSidebar, FiSmartphone, FiUser } from "react-icons/fi";
import {
FiArrowLeft,
FiGitPullRequest,
FiSidebar,
FiSmartphone,
FiUser,
} from "react-icons/fi";
import { useStore } from "zustand";
import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils";
@@ -11,7 +17,7 @@ import { useProjectContext } from "../context/project";
const StatusBar = ({ className }: React.ComponentProps<"div">) => {
const { user, urlPathname } = usePageContext();
const { isCompact } = useProjectContext();
const { isCompact, project } = useProjectContext();
const sidebarExpanded = useStore(sidebarStore, (i) => i.expanded);
const previewExpanded = useStore(previewStore, (i) => i.open);
@@ -22,24 +28,10 @@ const StatusBar = ({ className }: React.ComponentProps<"div">) => {
return (
<div
className={cn(
"h-10 flex items-center gap-1 pl-2 pr-3 w-full bg-slate-800 md:bg-[#242424] md:border-t border-slate-900 md:border-black/30",
"h-10 flex items-center gap-1 pr-3 w-full bg-slate-800 md:bg-[#242424] md:border-t border-slate-900 md:border-black/30",
className
)}
>
<ActionButton
title="Toggle Sidebar (CTRL+B)"
icon={FiSidebar}
className={sidebarExpanded ? "text-white" : ""}
onClick={() => sidebarStore.getState().toggle()}
/>
<ActionButton
title="Toggle Preview Window (CTRL+P)"
icon={FiSmartphone}
className={previewExpanded ? "text-white" : ""}
onClick={() => previewStore.getState().toggle()}
/>
<div className="flex-1"></div>
<ActionButton
title="Return to Home"
href="/"
@@ -54,6 +46,28 @@ const StatusBar = ({ className }: React.ComponentProps<"div">) => {
<FiUser className="text-sm" />
{user?.name || "Log in"}
</Button>
<Button
href={"/get-started?fork=" + project.slug}
className="h-full p-0 gap-2 text-xs ml-4 truncate"
variant="link"
>
<FiGitPullRequest className="text-sm" />
<span className="truncate">Fork Project</span>
</Button>
<div className="flex-1"></div>
<ActionButton
title="Toggle Sidebar (CTRL+B)"
icon={FiSidebar}
className={sidebarExpanded ? "text-white" : ""}
onClick={() => sidebarStore.getState().toggle()}
/>
<ActionButton
title="Toggle Preview Window (CTRL+P)"
icon={FiSmartphone}
className={previewExpanded ? "text-white" : ""}
onClick={() => previewStore.getState().toggle()}
/>
</div>
);
};