-
Notifications
You must be signed in to change notification settings - Fork 782
/
editor.tsx
130 lines (124 loc) · 5.21 KB
/
editor.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"use client";
import { useEffect, useState, useTransition } from "react";
import { updatePost, updatePostMetadata } from "@/lib/actions";
import { Editor as NovelEditor } from "novel";
import TextareaAutosize from "react-textarea-autosize";
import { cn } from "@/lib/utils";
import LoadingDots from "./icons/loading-dots";
import { ExternalLink } from "lucide-react";
import { toast } from "sonner";
import type { SelectPost } from "@/lib/schema";
type PostWithSite = SelectPost & { site: { subdomain: string | null } | null };
export default function Editor({ post }: { post: PostWithSite }) {
let [isPendingSaving, startTransitionSaving] = useTransition();
let [isPendingPublishing, startTransitionPublishing] = useTransition();
const [data, setData] = useState<PostWithSite>(post);
const [hydrated, setHydrated] = useState(false);
const url = process.env.NEXT_PUBLIC_VERCEL_ENV
? `https://${data.site?.subdomain}.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}/${data.slug}`
: `http://${data.site?.subdomain}.localhost:3000/${data.slug}`;
// listen to CMD + S and override the default behavior
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.metaKey && e.key === "s") {
e.preventDefault();
startTransitionSaving(async () => {
await updatePost(data);
});
}
};
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
};
}, [data, startTransitionSaving]);
return (
<div className="relative min-h-[500px] w-full max-w-screen-lg border-stone-200 p-12 px-8 sm:mb-[calc(20vh)] sm:rounded-lg sm:border sm:px-12 sm:shadow-lg dark:border-stone-700">
<div className="absolute right-5 top-5 mb-5 flex items-center space-x-3">
{data.published && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center space-x-1 text-sm text-stone-400 hover:text-stone-500"
>
<ExternalLink className="h-4 w-4" />
</a>
)}
<div className="rounded-lg bg-stone-100 px-2 py-1 text-sm text-stone-400 dark:bg-stone-800 dark:text-stone-500">
{isPendingSaving ? "Saving..." : "Saved"}
</div>
<button
onClick={() => {
const formData = new FormData();
console.log(data.published, typeof data.published);
formData.append("published", String(!data.published));
startTransitionPublishing(async () => {
await updatePostMetadata(formData, post.id, "published").then(
() => {
toast.success(
`Successfully ${
data.published ? "unpublished" : "published"
} your post.`,
);
setData((prev) => ({ ...prev, published: !prev.published }));
},
);
});
}}
className={cn(
"flex h-7 w-24 items-center justify-center space-x-2 rounded-lg border text-sm transition-all focus:outline-none",
isPendingPublishing
? "cursor-not-allowed border-stone-200 bg-stone-100 text-stone-400 dark:border-stone-700 dark:bg-stone-800 dark:text-stone-300"
: "border border-black bg-black text-white hover:bg-white hover:text-black active:bg-stone-100 dark:border-stone-700 dark:hover:border-stone-200 dark:hover:bg-black dark:hover:text-white dark:active:bg-stone-800",
)}
disabled={isPendingPublishing}
>
{isPendingPublishing ? (
<LoadingDots />
) : (
<p>{data.published ? "Unpublish" : "Publish"}</p>
)}
</button>
</div>
<div className="mb-5 flex flex-col space-y-3 border-b border-stone-200 pb-5 dark:border-stone-700">
<input
type="text"
placeholder="Title"
defaultValue={post?.title || ""}
autoFocus
onChange={(e) => setData({ ...data, title: e.target.value })}
className="dark:placeholder-text-600 border-none px-0 font-cal text-3xl placeholder:text-stone-400 focus:outline-none focus:ring-0 dark:bg-black dark:text-white"
/>
<TextareaAutosize
placeholder="Description"
defaultValue={post?.description || ""}
onChange={(e) => setData({ ...data, description: e.target.value })}
className="dark:placeholder-text-600 w-full resize-none border-none px-0 placeholder:text-stone-400 focus:outline-none focus:ring-0 dark:bg-black dark:text-white"
/>
</div>
<NovelEditor
className="relative block"
defaultValue={post?.content || undefined}
onUpdate={(editor) => {
setData((prev) => ({
...prev,
content: editor?.storage.markdown.getMarkdown(),
}));
}}
onDebouncedUpdate={() => {
if (
data.title === post.title &&
data.description === post.description &&
data.content === post.content
) {
return;
}
startTransitionSaving(async () => {
await updatePost(data);
});
}}
/>
</div>
);
}