feat: initial editor files, add toast

This commit is contained in:
Khairul Hidayat 2024-02-24 00:27:52 +00:00
parent 8bb3d2bd84
commit 49b80a2f4c
11 changed files with 108 additions and 22 deletions

27
components/ui/sonner.tsx Normal file
View File

@ -0,0 +1,27 @@
import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
theme="dark"
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-white group-[.toaster]:text-slate-950 group-[.toaster]:border-slate-200 group-[.toaster]:shadow-lg dark:group-[.toaster]:bg-slate-950 dark:group-[.toaster]:text-slate-50 dark:group-[.toaster]:border-slate-800",
description:
"group-[.toast]:text-slate-500 dark:group-[.toast]:text-slate-400",
actionButton:
"group-[.toast]:bg-slate-900 group-[.toast]:text-slate-50 dark:group-[.toast]:bg-slate-50 dark:group-[.toast]:text-slate-900",
cancelButton:
"group-[.toast]:bg-slate-100 group-[.toast]:text-slate-500 dark:group-[.toast]:bg-slate-800 dark:group-[.toast]:text-slate-400",
},
}}
{...props}
/>
);
};
export { Toaster };

View File

@ -1,5 +1,7 @@
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import copyToClipboard from "copy-to-clipboard";
import { toast } from "sonner";
import { BASE_URL } from "./consts"; import { BASE_URL } from "./consts";
import type { ProjectSchema } from "~/server/db/schema/project"; import type { ProjectSchema } from "~/server/db/schema/project";
import type { FileSchema } from "~/server/db/schema/file"; import type { FileSchema } from "~/server/db/schema/file";
@ -33,3 +35,14 @@ export const ucfirst = (str: string) => {
export const ucwords = (str: string) => { export const ucwords = (str: string) => {
return str.split(" ").map(ucfirst).join(" "); return str.split(" ").map(ucfirst).join(" ");
}; };
export const copy = (text: string) => {
copyToClipboard(text, {
onCopy: (data) => {
toast.success("Copied to clipboard!");
return data;
},
});
};
export { toast };

View File

@ -81,6 +81,7 @@
"react-hook-form": "^7.50.1", "react-hook-form": "^7.50.1",
"react-icons": "^5.0.1", "react-icons": "^5.0.1",
"react-resizable-panels": "^2.0.9", "react-resizable-panels": "^2.0.9",
"sonner": "^1.4.1",
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^2.14.0", "usehooks-ts": "^2.14.0",

View File

@ -4,6 +4,8 @@ import trpcServer from "~/server/api/trpc/trpc";
export const data = async (ctx: PageContext) => { export const data = async (ctx: PageContext) => {
const trpc = await trpcServer(ctx); const trpc = await trpcServer(ctx);
const searchParams = ctx.urlParsed.search;
const filesParam = searchParams.files ? searchParams.files.split(",") : null;
const project = await trpc.project.getById(ctx.routeParams?.slug!); const project = await trpc.project.getById(ctx.routeParams?.slug!);
if (!project) { if (!project) {
@ -11,14 +13,16 @@ export const data = async (ctx: PageContext) => {
} }
const files = await trpc.file.getAll({ projectId: project.id }); const files = await trpc.file.getAll({ projectId: project.id });
const pinnedFiles = files.filter((i) => i.isPinned); const initialFiles = files.filter((i) =>
filesParam != null ? filesParam.includes(i.path) : i.isPinned
);
return { return {
title: project.title, title: project.title,
description: `Check ${project.title} on CodeShare!`, description: `Check ${project.title} on CodeShare!`,
project, project,
files, files,
pinnedFiles, initialFiles,
}; };
}; };

View File

@ -6,7 +6,7 @@ const ConsoleLogger = () => {
const logs = useConsoleLogs(); const logs = useConsoleLogs();
return ( return (
<div className="h-full flex flex-col bg-[#242424] border-t border-t-gray-600"> <div className="h-full flex flex-col bg-[#242424] border-t border-gray-700">
<p className="py-2 px-3 uppercase text-xs">Console</p> <p className="py-2 px-3 uppercase text-xs">Console</p>
<ErrorBoundary> <ErrorBoundary>
<div className="overflow-y-auto flex-1"> <div className="overflow-y-auto flex-1">

View File

@ -20,21 +20,21 @@ import StatusBar from "./status-bar";
import { FiTerminal } from "react-icons/fi"; import { FiTerminal } from "react-icons/fi";
const Editor = () => { const Editor = () => {
const { project, pinnedFiles } = useData<Data>(); const { project, initialFiles } = useData<Data>();
const trpcUtils = trpc.useUtils(); const trpcUtils = trpc.useUtils();
const projectCtx = useProjectContext(); const projectCtx = useProjectContext();
const [breakpoint] = useBreakpoint(); const [breakpoint] = useBreakpoint();
const [curTabIdx, setCurTabIdx] = useState(0); const [curTabIdx, setCurTabIdx] = useState(0);
const [curOpenFiles, setOpenFiles] = useState<number[]>( const [curOpenFiles, setOpenFiles] = useState<number[]>(
pinnedFiles.map((i) => i.id) initialFiles.map((i) => i.id)
); );
const openedFilesData = trpc.file.getAll.useQuery( const openedFilesData = trpc.file.getAll.useQuery(
{ projectId: project.id, id: curOpenFiles }, { projectId: project.id, id: curOpenFiles },
{ enabled: curOpenFiles.length > 0, initialData: pinnedFiles } { enabled: curOpenFiles.length > 0, initialData: initialFiles }
); );
const [openedFiles, setOpenedFiles] = useState<any[]>(pinnedFiles); const [openedFiles, setOpenedFiles] = useState<any[]>(initialFiles);
const deleteFile = trpc.file.delete.useMutation({ const deleteFile = trpc.file.delete.useMutation({
onSuccess: (file) => { onSuccess: (file) => {
@ -49,11 +49,11 @@ const Editor = () => {
}); });
useEffect(() => { useEffect(() => {
if (!pinnedFiles?.length || curOpenFiles.length > 0) { if (!initialFiles?.length || curOpenFiles.length > 0) {
return; return;
} }
pinnedFiles.forEach((file) => { initialFiles.forEach((file) => {
onOpenFile(file.id, false); onOpenFile(file.id, false);
}); });
@ -61,7 +61,7 @@ const Editor = () => {
setOpenFiles([]); setOpenFiles([]);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [pinnedFiles]); }, [initialFiles]);
useEffect(() => { useEffect(() => {
if (openedFilesData.data) { if (openedFilesData.data) {
@ -170,7 +170,7 @@ const Editor = () => {
collapsedSize={0} collapsedSize={0}
/> />
<ResizableHandle className="bg-slate-900" /> <ResizableHandle className="w-0" />
<ResizablePanel defaultSize={{ sm: 100, md: 75 }}> <ResizablePanel defaultSize={{ sm: 100, md: 75 }}>
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical"> <ResizablePanelGroup autoSaveId="code-editor" direction="vertical">
@ -185,7 +185,7 @@ const Editor = () => {
{breakpoint >= 2 ? ( {breakpoint >= 2 ? (
<> <>
<ResizableHandle /> <ResizableHandle className="!h-0" />
<ResizablePanel <ResizablePanel
defaultSize={{ sm: 0, md: 20 }} defaultSize={{ sm: 0, md: 20 }}

View File

@ -19,9 +19,8 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuSeparator, DropdownMenuSeparator,
} from "~/components/ui/dropdown-menu"; } from "~/components/ui/dropdown-menu";
import { cn, getPreviewUrl } from "~/lib/utils"; import { cn, getPreviewUrl, getUrl, copy } from "~/lib/utils";
import FileIcon from "~/components/ui/file-icon"; import FileIcon from "~/components/ui/file-icon";
import copy from "copy-to-clipboard";
import { useData } from "~/renderer/hooks"; import { useData } from "~/renderer/hooks";
import Spinner from "~/components/ui/spinner"; import Spinner from "~/components/ui/spinner";
import { Data } from "../+data"; import { Data } from "../+data";
@ -197,6 +196,13 @@ const FileItem = ({ file, createFileDlg }: FileItemProps) => {
Open in new tab Open in new tab
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem
onClick={() =>
copy(getUrl(project.slug + `?files=${file.path}`))
}
>
Share
</DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
return updateFile.mutate({ return updateFile.mutate({

View File

@ -11,8 +11,8 @@ type Props = {
}; };
const FileViewer = ({ id }: Props) => { const FileViewer = ({ id }: Props) => {
const { pinnedFiles } = useData<Data>(); const { initialFiles } = useData<Data>();
const initialData = pinnedFiles.find((i) => i.id === id); const initialData = initialFiles.find((i) => i.id === id);
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, { const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, {
initialData, initialData,

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { FiSidebar, FiSmartphone, FiUser } from "react-icons/fi"; import { FiArrowLeft, FiSidebar, FiSmartphone, FiUser } from "react-icons/fi";
import { useStore } from "zustand"; import { useStore } from "zustand";
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
@ -22,24 +22,30 @@ const StatusBar = ({ className }: React.ComponentProps<"div">) => {
return ( return (
<div <div
className={cn( className={cn(
"h-10 flex items-center gap-1 pl-2 pr-3 w-full bg-slate-900 md:bg-[#242424] border-b md:border-b-0 md:border-t border-slate-900 md:border-black/30", "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",
className className
)} )}
> >
<ActionButton <ActionButton
title="Toggle Sidebar" title="Toggle Sidebar (CTRL+B)"
icon={FiSidebar} icon={FiSidebar}
className={sidebarExpanded ? "text-white" : ""} className={sidebarExpanded ? "text-white" : ""}
onClick={() => sidebarStore.getState().toggle()} onClick={() => sidebarStore.getState().toggle()}
/> />
<ActionButton <ActionButton
title="Toggle Preview Window" title="Toggle Preview Window (CTRL+P)"
icon={FiSmartphone} icon={FiSmartphone}
className={previewExpanded ? "text-white" : ""} className={previewExpanded ? "text-white" : ""}
onClick={() => previewStore.getState().toggle()} onClick={() => previewStore.getState().toggle()}
/> />
<div className="flex-1"></div> <div className="flex-1"></div>
<ActionButton
title="Return to Home"
href="/"
icon={FiArrowLeft}
size="lg"
/>
<Button <Button
href={user ? "/user" : "/auth/login?return=" + urlPathname} href={user ? "/user" : "/auth/login?return=" + urlPathname}
className="h-full p-0 gap-2 text-xs" className="h-full p-0 gap-2 text-xs"

13
pnpm-lock.yaml generated
View File

@ -131,6 +131,9 @@ dependencies:
react-resizable-panels: react-resizable-panels:
specifier: ^2.0.9 specifier: ^2.0.9
version: 2.0.9(react-dom@18.2.0)(react@18.2.0) version: 2.0.9(react-dom@18.2.0)(react@18.2.0)
sonner:
specifier: ^1.4.1
version: 1.4.1(react-dom@18.2.0)(react@18.2.0)
tailwind-merge: tailwind-merge:
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.1 version: 2.2.1
@ -5577,6 +5580,16 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/sonner@1.4.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-KJcFbMF+z2OMSJ9H+N6mrk/ffnEzuyLFlHoza/HQvNyiACoY958VtFdC7xD9D74ttzA+kcS1YIJOsNwbKWDsHw==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/sort-keys-length@1.0.1: /sort-keys-length@1.0.1:
resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}

View File

@ -1,14 +1,27 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import trpc, { getBaseUrl } from "~/lib/trpc"; import trpc, { getBaseUrl } from "~/lib/trpc";
import { toast } from "~/lib/utils";
import { httpBatchLink } from "@trpc/react-query"; import { httpBatchLink } from "@trpc/react-query";
import { Toaster } from "~/components/ui/sonner";
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
}; };
const Providers = ({ children }: Props) => { const Providers = ({ children }: Props) => {
const [queryClient] = useState(() => new QueryClient()); const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
mutations: {
onError(err) {
toast.error(err.message);
},
},
},
})
);
const [trpcClient] = useState(() => const [trpcClient] = useState(() =>
trpc.createClient({ trpc.createClient({
links: [ links: [
@ -24,7 +37,10 @@ const Providers = ({ children }: Props) => {
return ( return (
<trpc.Provider client={trpcClient} queryClient={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> <QueryClientProvider client={queryClient}>
{children}
<Toaster />
</QueryClientProvider>
</trpc.Provider> </trpc.Provider>
); );
}; };