mirror of
https://github.com/khairul169/code-share.git
synced 2025-04-28 16:49:36 +07:00
feat: initial editor files, add toast
This commit is contained in:
parent
8bb3d2bd84
commit
49b80a2f4c
27
components/ui/sonner.tsx
Normal file
27
components/ui/sonner.tsx
Normal 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 };
|
13
lib/utils.ts
13
lib/utils.ts
@ -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 };
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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 }}
|
||||||
|
@ -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({
|
||||||
|
@ -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,
|
||||||
|
@ -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
13
pnpm-lock.yaml
generated
@ -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'}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user