mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
add wiki edit view
This commit is contained in:
parent
3c57aca4a9
commit
b7572d373e
@ -2,4 +2,4 @@
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add simple wiki pages
|
||||
Add wiki pages
|
||||
|
@ -2,4 +2,4 @@
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add date picker to notifictions
|
||||
Add date picker to notifications
|
||||
|
@ -126,6 +126,7 @@ const WikiTopicView = lazy(() => import("./views/wiki/topic"));
|
||||
const WikiSearchView = lazy(() => import("./views/wiki/search"));
|
||||
const WikiCompareView = lazy(() => import("./views/wiki/compare"));
|
||||
const CreateWikiPageView = lazy(() => import("./views/wiki/create"));
|
||||
const EditWikiPageView = lazy(() => import("./views/wiki/edit"));
|
||||
|
||||
const overrideReactTextareaAutocompleteStyles = css`
|
||||
.rta__autocomplete {
|
||||
@ -321,6 +322,7 @@ const router = createHashRouter([
|
||||
{ path: "search", element: <WikiSearchView /> },
|
||||
{ path: "topic/:topic", element: <WikiTopicView /> },
|
||||
{ path: "page/:naddr", element: <WikiPageView /> },
|
||||
{ path: "edit/:topic", element: <EditWikiPageView /> },
|
||||
{ path: "compare/:topic/:a/:b", element: <WikiCompareView /> },
|
||||
{ path: "create", element: <CreateWikiPageView /> },
|
||||
{ path: "", element: <WikiHomeView /> },
|
||||
|
88
src/views/wiki/components/markdown-editor.tsx
Normal file
88
src/views/wiki/components/markdown-editor.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { VisuallyHidden } from "@chakra-ui/react";
|
||||
import SimpleMDE, { SimpleMDEReactProps } from "react-simplemde-editor";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
|
||||
import EasyMDE from "easymde";
|
||||
import "easymde/dist/easymde.min.css";
|
||||
|
||||
import useUsersMediaServers from "../../../hooks/use-user-media-servers";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
|
||||
import { CharkaMarkdown } from "./markdown";
|
||||
import { useSigningContext } from "../../../providers/global/signing-provider";
|
||||
import { uploadFileToServers } from "../../../helpers/media-upload/blossom";
|
||||
|
||||
export default function MarkdownEditor({ options, ...props }: SimpleMDEReactProps) {
|
||||
const account = useCurrentAccount();
|
||||
const { requestSignature } = useSigningContext();
|
||||
const { mediaUploadService } = useAppSettings();
|
||||
const { servers } = useUsersMediaServers(account?.pubkey);
|
||||
|
||||
const [_, setPreview] = useState<HTMLElement>();
|
||||
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||
const customOptions = useMemo(() => {
|
||||
const uploads = mediaUploadService === "blossom";
|
||||
async function imageUploadFunction(file: File, onSuccess: (url: string) => void, onError: (error: string) => void) {
|
||||
if (!servers) return onError("No media servers set");
|
||||
try {
|
||||
const blob = await uploadFileToServers(servers, file, requestSignature);
|
||||
if (blob) onSuccess(blob.url);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) onError(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
minHeight: "60vh",
|
||||
uploadImage: uploads,
|
||||
imageUploadFunction: uploads ? imageUploadFunction : undefined,
|
||||
toolbar: [
|
||||
"undo",
|
||||
"redo",
|
||||
"|",
|
||||
"bold",
|
||||
"italic",
|
||||
"heading",
|
||||
"|",
|
||||
"quote",
|
||||
"unordered-list",
|
||||
"ordered-list",
|
||||
"table",
|
||||
"code",
|
||||
"|",
|
||||
"link",
|
||||
"image",
|
||||
...(uploads
|
||||
? [
|
||||
{
|
||||
name: "upload-image",
|
||||
title: "Upload Image",
|
||||
className: "fa fa-upload",
|
||||
action: EasyMDE.drawUploadedImage,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
"|",
|
||||
"preview",
|
||||
"side-by-side",
|
||||
"fullscreen",
|
||||
"|",
|
||||
"guide",
|
||||
],
|
||||
previewRender(text, element) {
|
||||
return previewRef.current?.innerHTML || ReactDOMServer.renderToString(<CharkaMarkdown>{text}</CharkaMarkdown>);
|
||||
},
|
||||
} satisfies SimpleMDEReactProps["options"];
|
||||
}, [servers, requestSignature, setPreview]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SimpleMDE options={customOptions} {...props} />
|
||||
<VisuallyHidden>
|
||||
<CharkaMarkdown ref={previewRef}>{props.value ?? ""}</CharkaMarkdown>
|
||||
</VisuallyHidden>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,19 +1,29 @@
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { MenuItem } from "@chakra-ui/react";
|
||||
|
||||
import { DotsMenuButton, MenuIconButtonProps } from "../../../components/dots-menu-button";
|
||||
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||
import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link";
|
||||
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||
import DebugEventMenuItem from "../../../components/debug-modal/debug-event-menu-item";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { EditIcon } from "../../../components/icons";
|
||||
import { getPageTopic } from "../../../helpers/nostr/wiki";
|
||||
|
||||
export default function WikiPageMenu({ page, ...props }: { page: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||
const account = useCurrentAccount();
|
||||
|
||||
export default function WikiPageMenu({
|
||||
page,
|
||||
...props
|
||||
}: { page: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||
return (
|
||||
<>
|
||||
<DotsMenuButton {...props}>
|
||||
<OpenInAppMenuItem event={page} />
|
||||
{account?.pubkey === page.pubkey && (
|
||||
<MenuItem as={RouterLink} to={`/wiki/edit/${getPageTopic(page)}`} icon={<EditIcon />}>
|
||||
Edit Page
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<CopyShareLinkMenuItem event={page} />
|
||||
<CopyEmbedCodeMenuItem event={page} />
|
||||
|
||||
|
@ -23,6 +23,7 @@ import { useSigningContext } from "../../providers/global/signing-provider";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { CharkaMarkdown } from "./components/markdown";
|
||||
import useCacheForm from "../../hooks/use-cache-form";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
|
||||
export default function CreateWikiPageView() {
|
||||
const account = useCurrentAccount();
|
||||
@ -63,6 +64,7 @@ export default function CreateWikiPageView() {
|
||||
tags: [
|
||||
["d", values.topic],
|
||||
["title", values.title],
|
||||
["published_at", String(dayjs().unix())],
|
||||
],
|
||||
created_at: dayjs().unix(),
|
||||
};
|
||||
@ -152,7 +154,7 @@ export default function CreateWikiPageView() {
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<Input {...register("title", { required: true })} />
|
||||
<Input {...register("title", { required: true })} autoComplete="off" />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<SimpleMDE value={getValues().content} onChange={(v) => setValue("content", v)} options={options} />
|
||||
|
98
src/views/wiki/edit.tsx
Normal file
98
src/views/wiki/edit.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { Button, Flex, FormControl, FormLabel, Heading, Input, Spinner, useToast } from "@chakra-ui/react";
|
||||
import { Navigate, useNavigate, useParams } from "react-router-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
|
||||
import { WIKI_RELAYS } from "../../const";
|
||||
import useCacheForm from "../../hooks/use-cache-form";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { WIKI_PAGE_KIND, getPageTitle, getPageTopic } from "../../helpers/nostr/wiki";
|
||||
import { getSharableEventAddress } from "../../helpers/nip19";
|
||||
import { usePublishEvent } from "../../providers/global/publish-provider";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import MarkdownEditor from "./components/markdown-editor";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import { cloneEvent } from "../../helpers/nostr/event";
|
||||
|
||||
function EditWikiPagePage({ page }: { page: NostrEvent }) {
|
||||
const toast = useToast();
|
||||
const publish = usePublishEvent();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const topic = getPageTopic(page);
|
||||
|
||||
const { register, setValue, getValues, handleSubmit, watch, formState, reset } = useForm({
|
||||
defaultValues: { content: page.content, title: getPageTitle(page) ?? topic },
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const clearFormCache = useCacheForm(
|
||||
"wiki-" + topic,
|
||||
// @ts-expect-error
|
||||
getValues,
|
||||
setValue,
|
||||
formState,
|
||||
);
|
||||
|
||||
watch("content");
|
||||
register("content", {
|
||||
minLength: 10,
|
||||
required: true,
|
||||
});
|
||||
|
||||
const submit = handleSubmit(async (values) => {
|
||||
try {
|
||||
const draft = cloneEvent(WIKI_PAGE_KIND, page);
|
||||
draft.content = values.content;
|
||||
|
||||
const pub = await publish("Publish Page", draft, WIKI_RELAYS, false);
|
||||
clearFormCache();
|
||||
navigate(`/wiki/page/${getSharableEventAddress(pub.event)}`);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) toast({ description: error.message, status: "error" });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<VerticalPageLayout as="form" h="full" onSubmit={submit}>
|
||||
<Heading>Create Page</Heading>
|
||||
<Flex gap="2" wrap={{ base: "wrap", md: "nowrap" }}>
|
||||
<FormControl w={{ base: "full", md: "sm" }} isRequired flexShrink={0}>
|
||||
<FormLabel>Topic</FormLabel>
|
||||
<Input readOnly value={topic} />
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<FormLabel>Title</FormLabel>
|
||||
<Input {...register("title", { required: true })} autoComplete="off" />
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<MarkdownEditor value={getValues().content} onChange={(v) => setValue("content", v)} />
|
||||
<Flex gap="2" justifyContent="flex-end">
|
||||
{formState.isDirty && <Button onClick={() => reset()}>Clear</Button>}
|
||||
<Button onClick={() => navigate(-1)}>Cancel</Button>
|
||||
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
|
||||
Publish
|
||||
</Button>
|
||||
</Flex>
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EditWikiPageView() {
|
||||
const account = useCurrentAccount();
|
||||
if (!account) return <Navigate to="/" />;
|
||||
|
||||
const topic = useParams().topic;
|
||||
if (!topic) return <Navigate to="/wiki" />;
|
||||
|
||||
const page = useReplaceableEvent({ kind: WIKI_PAGE_KIND, pubkey: account.pubkey, identifier: topic });
|
||||
|
||||
if (!page) return <Spinner />;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<EditWikiPagePage page={page} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
@ -25,7 +25,6 @@ import useSubject from "../../hooks/use-subject";
|
||||
import useWikiTopicTimeline from "./hooks/use-wiki-topic-timeline";
|
||||
import WikiPageResult from "./components/wiki-page-result";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import DebugEventButton from "../../components/debug-modal/debug-event-button";
|
||||
import WikiPageHeader from "./components/wiki-page-header";
|
||||
import { WIKI_RELAYS } from "../../const";
|
||||
import GitBranch01 from "../../components/icons/git-branch-01";
|
||||
@ -36,6 +35,7 @@ import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbl
|
||||
import QuoteRepostButton from "../../components/note/quote-repost-button";
|
||||
import WikiPageMenu from "./components/wioki-page-menu";
|
||||
import EventVoteButtons from "../../components/reactions/event-vote-buttions";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
|
||||
function ForkAlert({ page, address }: { page: NostrEvent; address: nip19.AddressPointer }) {
|
||||
const topic = getPageTopic(page);
|
||||
@ -85,6 +85,7 @@ function DeferAlert({ page, address }: { page: NostrEvent; address: nip19.Addres
|
||||
}
|
||||
|
||||
function WikiPagePage({ page }: { page: NostrEvent }) {
|
||||
const account = useCurrentAccount();
|
||||
const topic = getPageTopic(page);
|
||||
const timeline = useWikiTopicTimeline(topic);
|
||||
|
||||
@ -105,20 +106,29 @@ function WikiPagePage({ page }: { page: NostrEvent }) {
|
||||
<VerticalPageLayout>
|
||||
<WikiPageHeader />
|
||||
|
||||
<Flex wrap="wrap">
|
||||
<Box>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
<Box flex={1}>
|
||||
<Heading>{getPageTitle(page)}</Heading>
|
||||
<Text>
|
||||
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
|
||||
</Text>
|
||||
</Box>
|
||||
<Flex alignItems="flex-end" gap="2" ml="auto">
|
||||
<EventVoteButtons event={page} inline chevrons={false} />
|
||||
<ButtonGroup size="sm">
|
||||
<QuoteRepostButton event={page} />
|
||||
<NoteZapButton event={page} showEventPreview={false} />
|
||||
<WikiPageMenu page={page} aria-label="Page Options" />
|
||||
<Flex direction="column" gap="2" ml="auto">
|
||||
<ButtonGroup ml="auto" size="sm">
|
||||
{page.pubkey === account?.pubkey && (
|
||||
<Button as={RouterLink} colorScheme="primary" to={`/wiki/edit/${getPageTopic(page)}`}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
<Flex alignItems="flex-end" gap="2" ml="auto">
|
||||
<EventVoteButtons event={page} inline chevrons={false} />
|
||||
<ButtonGroup size="sm">
|
||||
<QuoteRepostButton event={page} />
|
||||
<NoteZapButton event={page} showEventPreview={false} />
|
||||
<WikiPageMenu page={page} aria-label="Page Options" />
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{address && <ForkAlert page={page} address={address} />}
|
||||
|
Loading…
x
Reference in New Issue
Block a user