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 { twMerge } from "tailwind-merge";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import { toast } from "sonner";
|
||||
import { BASE_URL } from "./consts";
|
||||
import type { ProjectSchema } from "~/server/db/schema/project";
|
||||
import type { FileSchema } from "~/server/db/schema/file";
|
||||
@ -33,3 +35,14 @@ export const ucfirst = (str: string) => {
|
||||
export const ucwords = (str: string) => {
|
||||
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-icons": "^5.0.1",
|
||||
"react-resizable-panels": "^2.0.9",
|
||||
"sonner": "^1.4.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^2.14.0",
|
||||
|
@ -4,6 +4,8 @@ import trpcServer from "~/server/api/trpc/trpc";
|
||||
|
||||
export const data = async (ctx: PageContext) => {
|
||||
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!);
|
||||
if (!project) {
|
||||
@ -11,14 +13,16 @@ export const data = async (ctx: PageContext) => {
|
||||
}
|
||||
|
||||
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 {
|
||||
title: project.title,
|
||||
description: `Check ${project.title} on CodeShare!`,
|
||||
project,
|
||||
files,
|
||||
pinnedFiles,
|
||||
initialFiles,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -6,7 +6,7 @@ const ConsoleLogger = () => {
|
||||
const logs = useConsoleLogs();
|
||||
|
||||
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>
|
||||
<ErrorBoundary>
|
||||
<div className="overflow-y-auto flex-1">
|
||||
|
@ -20,21 +20,21 @@ import StatusBar from "./status-bar";
|
||||
import { FiTerminal } from "react-icons/fi";
|
||||
|
||||
const Editor = () => {
|
||||
const { project, pinnedFiles } = useData<Data>();
|
||||
const { project, initialFiles } = useData<Data>();
|
||||
const trpcUtils = trpc.useUtils();
|
||||
const projectCtx = useProjectContext();
|
||||
const [breakpoint] = useBreakpoint();
|
||||
|
||||
const [curTabIdx, setCurTabIdx] = useState(0);
|
||||
const [curOpenFiles, setOpenFiles] = useState<number[]>(
|
||||
pinnedFiles.map((i) => i.id)
|
||||
initialFiles.map((i) => i.id)
|
||||
);
|
||||
|
||||
const openedFilesData = trpc.file.getAll.useQuery(
|
||||
{ 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({
|
||||
onSuccess: (file) => {
|
||||
@ -49,11 +49,11 @@ const Editor = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!pinnedFiles?.length || curOpenFiles.length > 0) {
|
||||
if (!initialFiles?.length || curOpenFiles.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pinnedFiles.forEach((file) => {
|
||||
initialFiles.forEach((file) => {
|
||||
onOpenFile(file.id, false);
|
||||
});
|
||||
|
||||
@ -61,7 +61,7 @@ const Editor = () => {
|
||||
setOpenFiles([]);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pinnedFiles]);
|
||||
}, [initialFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
if (openedFilesData.data) {
|
||||
@ -170,7 +170,7 @@ const Editor = () => {
|
||||
collapsedSize={0}
|
||||
/>
|
||||
|
||||
<ResizableHandle className="bg-slate-900" />
|
||||
<ResizableHandle className="w-0" />
|
||||
|
||||
<ResizablePanel defaultSize={{ sm: 100, md: 75 }}>
|
||||
<ResizablePanelGroup autoSaveId="code-editor" direction="vertical">
|
||||
@ -185,7 +185,7 @@ const Editor = () => {
|
||||
|
||||
{breakpoint >= 2 ? (
|
||||
<>
|
||||
<ResizableHandle />
|
||||
<ResizableHandle className="!h-0" />
|
||||
|
||||
<ResizablePanel
|
||||
defaultSize={{ sm: 0, md: 20 }}
|
||||
|
@ -19,9 +19,8 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} 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 copy from "copy-to-clipboard";
|
||||
import { useData } from "~/renderer/hooks";
|
||||
import Spinner from "~/components/ui/spinner";
|
||||
import { Data } from "../+data";
|
||||
@ -197,6 +196,13 @@ const FileItem = ({ file, createFileDlg }: FileItemProps) => {
|
||||
Open in new tab
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
copy(getUrl(project.slug + `?files=${file.path}`))
|
||||
}
|
||||
>
|
||||
Share
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
return updateFile.mutate({
|
||||
|
@ -11,8 +11,8 @@ type Props = {
|
||||
};
|
||||
|
||||
const FileViewer = ({ id }: Props) => {
|
||||
const { pinnedFiles } = useData<Data>();
|
||||
const initialData = pinnedFiles.find((i) => i.id === id);
|
||||
const { initialFiles } = useData<Data>();
|
||||
const initialData = initialFiles.find((i) => i.id === id);
|
||||
|
||||
const { data, isLoading, refetch } = trpc.file.getById.useQuery(id, {
|
||||
initialData,
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { Button } from "~/components/ui/button";
|
||||
import { cn } from "~/lib/utils";
|
||||
@ -22,24 +22,30 @@ 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-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
|
||||
)}
|
||||
>
|
||||
<ActionButton
|
||||
title="Toggle Sidebar"
|
||||
title="Toggle Sidebar (CTRL+B)"
|
||||
icon={FiSidebar}
|
||||
className={sidebarExpanded ? "text-white" : ""}
|
||||
onClick={() => sidebarStore.getState().toggle()}
|
||||
/>
|
||||
<ActionButton
|
||||
title="Toggle Preview Window"
|
||||
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="/"
|
||||
icon={FiArrowLeft}
|
||||
size="lg"
|
||||
/>
|
||||
<Button
|
||||
href={user ? "/user" : "/auth/login?return=" + urlPathname}
|
||||
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:
|
||||
specifier: ^2.0.9
|
||||
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:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
@ -5577,6 +5580,16 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
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:
|
||||
resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -1,14 +1,27 @@
|
||||
import React, { useState } from "react";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import trpc, { getBaseUrl } from "~/lib/trpc";
|
||||
import { toast } from "~/lib/utils";
|
||||
import { httpBatchLink } from "@trpc/react-query";
|
||||
import { Toaster } from "~/components/ui/sonner";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
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(() =>
|
||||
trpc.createClient({
|
||||
links: [
|
||||
@ -24,7 +37,10 @@ const Providers = ({ children }: Props) => {
|
||||
|
||||
return (
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user