Skip to content

Commit

Permalink
feat(draft): develop draft create, save and publish action
Browse files Browse the repository at this point in the history
  • Loading branch information
lunarianss committed Oct 29, 2024
1 parent e2720d5 commit 39606ac
Show file tree
Hide file tree
Showing 16 changed files with 9,655 additions and 1,946 deletions.
12 changes: 11 additions & 1 deletion apps/web-admin/app/editor/[draft]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
"use client";
import "@benjamin/ui/benjamin-ui-global.css";
import { SnackbarProvider } from "notistack";

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <main className="px-4 py-6">{children}</main>;
return (
<SnackbarProvider
preventDuplicate
autoHideDuration={2000}
anchorOrigin={{ vertical: "top", horizontal: "center" }}
>
<main className="px-4 py-6">{children}</main>;
</SnackbarProvider>
);
}
3 changes: 2 additions & 1 deletion apps/web-admin/app/editor/[draft]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ export default function EditorPage({
params: Promise<{ draft: string }>;
}) {
const p = use(params);

return (
<div>
<Editor draftId={p.draft} content={undefined} />
<Editor draftId={p.draft} />
</div>
);
}
7 changes: 5 additions & 2 deletions apps/web-admin/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { EditorStoreProvider } from "@/providers/editorProvider";
import { EditorStoreProvider, ReactQueryProvider } from "@/providers";
import "@benjamin/ui/benjamin-ui-global.css";
import type { Metadata } from "next";
import localFont from "next/font/local";

import "./globals.css";
const geistSans = localFont({
src: "./fonts/GeistVF.woff",
Expand Down Expand Up @@ -29,7 +30,9 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<EditorStoreProvider>{children}</EditorStoreProvider>
<ReactQueryProvider>
<EditorStoreProvider>{children}</EditorStoreProvider>
</ReactQueryProvider>
</body>
</html>
);
Expand Down
31 changes: 30 additions & 1 deletion apps/web-admin/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
"use client";
import { useCreateDraft } from "@/server/post";
import { Button } from "@benjamin/ui";
import { Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";

export default function Home() {
return <div></div>;
const { mutateAsync: createDraftApi, isPending } = useCreateDraft();

const router = useRouter();

const handlePublish = async () => {
try {
const draftData = await createDraftApi();
router.push(`/editor/${draftData.id}`);
} catch (err) {
// todo
}
};

return (
<div className="w-content mx-auto py-6 px-3 flex">
<Button onClick={handlePublish} disabled={isPending}>
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
发文章
</Button>
<Button className="mx-5">管理文章</Button>
<Button>管理标签</Button>
<Button className="mx-5">管理草稿</Button>
</div>
);
}
148 changes: 101 additions & 47 deletions apps/web-admin/components/editor/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
"use client";

import { useEditorStore } from "@/providers/editorProvider";
import { useGetDraftDetail, useUpdateDraftDetail } from "@/server/post";
import { Button } from "@benjamin/ui";
import EditorJS, { OutputData } from "@editorjs/editorjs";
import EditorJS from "@editorjs/editorjs";
import { ArrowLeftIcon } from "@radix-ui/react-icons";
import { Loader2 } from "lucide-react";
import Link from "next/link";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useRef, useState } from "react";
import TextareaAutosize from "react-textarea-autosize";

interface EditorProps {
draftId?: string;
content: OutputData | undefined;
}
export default function Editor({ draftId, content }: EditorProps) {

export default function Editor({ draftId }: EditorProps) {
const [isMounted, setIsMounted] = useState<boolean>(false);
const [isSaving, setIsSaving] = useState<boolean>(false);
const { updateTitle, title } = useEditorStore((state) => state);
const { mutateAsync: getDraftDetail } = useGetDraftDetail();
const { updateTitle, title, content, updateContent } = useEditorStore(
(state) => state
);
const [isInitialized, setInitialState] = useState(false);
const { enqueueSnackbar } = useSnackbar();
const { mutateAsync: updateDraftDetail } = useUpdateDraftDetail();
const ref = useRef<EditorJS>();

const initializeEditor = useCallback(async () => {
Expand All @@ -32,8 +42,8 @@ export default function Editor({ draftId, content }: EditorProps) {
holder: "editor",
onReady() {
ref.current = editor;
setInitialState(true);
},
data: content,
placeholder: "Type here to write your post...",
inlineToolbar: true,
tools: {
Expand All @@ -47,79 +57,123 @@ export default function Editor({ draftId, content }: EditorProps) {
},
});
}
}, [content]);
}, []);

// draft init
useEffect(() => {
initializeEditor();
return () => {
ref.current?.destroy();
ref.current = undefined;
const draftDetailAsync = async () => {
if (draftId) {
const data = await getDraftDetail(Number.parseInt(draftId));
updateTitle(data.title);
if (data.content && !Object.keys(data.content).length) {
data.content = undefined;
}
updateContent(data.content);
enqueueSnackbar({ message: "草稿初始化成功", variant: "success" });
}
};
}, [initializeEditor]);
draftDetailAsync();
}, [draftId, getDraftDetail, updateTitle, updateContent, enqueueSnackbar]);

// render
useEffect(() => {
if (
ref.current &&
content &&
Object.keys(content).length &&
isInitialized
) {
ref.current.render(content);
}
}, [content, isInitialized]);

// render
useEffect(() => {
if (typeof window !== "undefined") {
setIsMounted(true);
}
}, []);

useEffect(() => {
if (isMounted) {
initializeEditor();

return () => {
ref.current?.destroy();
ref.current = undefined;
};
}
}, [isMounted, initializeEditor]);

// 保存草稿
const handleSave = async () => {
setIsSaving(true);
const data = await ref.current?.save();

const content = await ref.current?.save();
if (draftId) {
console.log(draftId, "draftId");
console.log(data, "content");
console.log(title, "title");
try {
await updateDraftDetail({
id: Number.parseInt(draftId),
title,
content,
});
enqueueSnackbar({ message: "草稿保存成功", variant: "success" });
} catch (err) {
console.log(err);
}
}
setIsSaving(false);
};

// 草稿模式下发布文章
// const handlePublish = async () => {
// setIsSaving(true);
// const data = await ref.current?.save();

// if (draftId) {
// console.log(draftId, "draftId");
// console.log(data, "content");
// console.log(title, "title");
// }
// setIsSaving(false);
// };

return (
<div className="grid w-full gap-10">
<div className="flex w-auto items-center justify-between">
<div className="flex items-center space-x-10">
<Button>
<>
<ArrowLeftIcon />
Back
</>
<Button asChild>
<Link href="/">
<>
<ArrowLeftIcon />
Back
</>
</Link>
</Button>

<p className="text-sm text-muted-foreground">
{0 ? "Published" : "Draft"}
{" "}
<span className="bg-green-500 rounded-full w-2 h-2 inline-block mr-2"></span>
Draft
</p>

<p className="text-sm text-gray-700 bg-gray-100 p-2 rounded-md">
Input{" "}
<kbd className="rounded-md border bg-muted px-1 text-xs uppercase">
/
</kbd>{" "}
to open the command menu.
</p>
</div>
<div>
<Button onClick={handleSave} variant="outline">
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<span>Publish</span>
</Button>

<Button onClick={handleSave} className="ml-3">
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<span>Save</span>
</Button>
</div>
<Button onClick={handleSave}>
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<span>Save</span>
</Button>
</div>
<div className="prose prose-stone mx-auto w-[800px] dark:prose-invert">
<TextareaAutosize
title={title}
defaultValue={title}
autoFocus
id="title"
defaultValue={"假如旷野是假象"}
placeholder="Post title"
onChange={(e) => updateTitle(e.target.value)}
className="w-full resize-none appearance-none overflow-hidden bg-transparent text-4xl font-bold focus:outline-none"
/>
<div id="editor" className="min-h-[500px]" />
<p className="text-sm text-gray-500">
Input{" "}
<kbd className="rounded-md border bg-muted px-1 text-xs uppercase">
/
</kbd>{" "}
to open the command menu.
</p>
</div>
</div>
);
Expand Down
7 changes: 6 additions & 1 deletion apps/web-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
"dev": "next dev -p 3001 --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@benjamin/ui": "workspace:^",
"@radix-ui/react-icons": "^1.3.0",
"@tanstack/react-query": "^5.59.15",
"@tanstack/react-query-devtools": "^5.59.15",
"@tanstack/react-query-next-experimental": "^5.59.15",
"axios": "^1.7.7",
"lucide-react": "^0.453.0",
"next": "15.0.1",
"notistack": "^3.0.1",
"react": "19.0.0-rc-69d4b800-20241021",
"react-dom": "19.0.0-rc-69d4b800-20241021",
"zustand": "^5.0.0"
Expand Down
2 changes: 2 additions & 0 deletions apps/web-admin/providers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { EditorStoreProvider } from "./editorProvider";
export { ReactQueryProvider } from "./queryProvider";
43 changes: 43 additions & 0 deletions apps/web-admin/providers/queryProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";
import {
QueryClient,
QueryClientProvider,
isServer,
} from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
import * as React from "react";

function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
});
}

let browserQueryClient: QueryClient | undefined = undefined;

function getQueryClient() {
if (isServer) {
return makeQueryClient();
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
}

export function ReactQueryProvider(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();

return (
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration>
{props.children}
</ReactQueryStreamedHydration>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
2 changes: 2 additions & 0 deletions apps/web-admin/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useInfiniteQueryPost } from "./post/";
export { client } from "./request/";
Loading

0 comments on commit 39606ac

Please sign in to comment.