From b1237730bbc9223d927d1868caf6c236288cbea6 Mon Sep 17 00:00:00 2001 From: Niaz Date: Thu, 14 Dec 2023 10:09:11 +0100 Subject: [PATCH] Add RTE editor to description --- package-lock.json | 33 ++++ package.json | 6 + src/components/lightbox/Description.tsx | 204 ++++++++++++++++++++++++ src/components/lightbox/MentionList.tsx | 93 +++++++++++ src/components/lightbox/Sidebar.tsx | 158 +----------------- src/components/lightbox/Suggestion.tsx | 92 +++++++++++ 6 files changed, 434 insertions(+), 152 deletions(-) create mode 100644 src/components/lightbox/Description.tsx create mode 100644 src/components/lightbox/MentionList.tsx create mode 100644 src/components/lightbox/Suggestion.tsx diff --git a/package-lock.json b/package-lock.json index e522464b..43dddbd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,10 +24,15 @@ "@popperjs/core": "2.11.8", "@reduxjs/toolkit": "1.9.7", "@tabler/icons-react": "^2.44.0", + "@tiptap/extension-document": "^2.1.13", "@tiptap/extension-link": "^2.1.13", + "@tiptap/extension-mention": "^2.1.13", + "@tiptap/extension-paragraph": "^2.1.13", + "@tiptap/extension-text": "^2.1.13", "@tiptap/pm": "^2.1.13", "@tiptap/react": "^2.1.13", "@tiptap/starter-kit": "^2.1.13", + "@tiptap/suggestion": "^2.1.13", "@visx/gradient": "3.3.0", "@visx/group": "3.3.0", "@visx/hierarchy": "3.3.0", @@ -94,6 +99,7 @@ "redux-thunk": "2.4.2", "rumble-charts": "5.0.0", "source-map-explorer": "2.5.3", + "tippy.js": "^6.3.7", "zod": "3.22.4" }, "devDependencies": { @@ -5745,6 +5751,20 @@ "@tiptap/core": "^2.0.0" } }, + "node_modules/@tiptap/extension-mention": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.1.13.tgz", + "integrity": "sha512-OYqaucyBiCN/CmDYjpOVX74RJcIEKmAqiZxUi8Gfaq7ryEO5a8Gk93nK+8uZ0onaqHE+mHpoLFFbcAFbOPgkUQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0", + "@tiptap/suggestion": "^2.0.0" + } + }, "node_modules/@tiptap/extension-ordered-list": { "version": "2.1.13", "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.1.13.tgz", @@ -5871,6 +5891,19 @@ "url": "https://github.com/sponsors/ueberdosis" } }, + "node_modules/@tiptap/suggestion": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@tiptap/suggestion/-/suggestion-2.1.13.tgz", + "integrity": "sha512-Y05TsiXTFAJ5SrfoV+21MAxig5UNbY0AVa03lQlh/yicTRPpIc6hgZzblB0uxDSYoj6+kaHE4MIZvPvhUD8BJQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0", + "@tiptap/pm": "^2.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", diff --git a/package.json b/package.json index a12a04f4..b58a8a03 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,12 @@ "@tiptap/pm": "^2.1.13", "@tiptap/react": "^2.1.13", "@tiptap/starter-kit": "^2.1.13", + "@tiptap/extension-mention": "^2.1.13", + "@tiptap/extension-document": "^2.1.13", + "@tiptap/extension-paragraph": "^2.1.13", + "@tiptap/extension-text": "^2.1.13", + "@tiptap/suggestion": "^2.1.13", + "tippy.js": "^6.3.7", "@visx/gradient": "3.3.0", "@visx/group": "3.3.0", "@visx/hierarchy": "3.3.0", diff --git a/src/components/lightbox/Description.tsx b/src/components/lightbox/Description.tsx new file mode 100644 index 00000000..1ea8c0b3 --- /dev/null +++ b/src/components/lightbox/Description.tsx @@ -0,0 +1,204 @@ +import { ActionIcon, Badge, Group, Stack, Text, Title, Tooltip, UnstyledButton } from "@mantine/core"; +import { RichTextEditor } from "@mantine/tiptap"; +import { IconCheck, IconEdit, IconX, IconNote as Note, IconTags as Tags, IconWand as Wand } from "@tabler/icons-react"; +import Document from "@tiptap/extension-document"; +import Mention from "@tiptap/extension-mention"; +import Paragraph from "@tiptap/extension-paragraph"; +import { Text as TipTapText } from "@tiptap/extension-text"; +import { useEditor } from "@tiptap/react"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { push } from "redux-first-history"; + +import { generatePhotoIm2txtCaption, savePhotoCaption } from "../../actions/photosActions"; +import type { Photo as PhotoType } from "../../actions/photosActions.types"; +import { useAppDispatch, useAppSelector } from "../../store/store"; +import suggestion from "./Suggestion"; + +type Props = { + isPublic: boolean; + photoDetail: PhotoType; +}; +export function Description(props: Props) { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const { generatingCaptionIm2txt } = useAppSelector(store => store.photos); + const { photoDetail, isPublic } = props; + + const [editMode, setEditMode] = useState(false); + const [imageCaption, setImageCaption] = useState(""); + const editor = useEditor({ + editable: editMode, + extensions: [ + Document, + Paragraph, + TipTapText, + Mention.configure({ + HTMLAttributes: { + style: "border: 1px solid #000; border-radius: 0.4rem; padding: 0.1rem 0.3rem; box-decoration-break: clone;", + }, + suggestion, + }), + ], + content: imageCaption, + // eslint-disable-next-line @typescript-eslint/no-shadow + onUpdate({ editor }) { + setImageCaption(editor.getText()); + }, + }); + + useEffect(() => { + if (photoDetail) { + const currentCaption = photoDetail.captions_json.user_caption ? photoDetail.captions_json.user_caption : ""; + const replacedCaption = currentCaption.replace( + /#(\w+)/g, + '#$1' + ); + editor?.commands.setContent(replacedCaption); + setImageCaption(currentCaption); + } + }, [photoDetail, editor]); + + return ( + + + + + + {t("lightbox.sidebar.caption")} + + {photoDetail.captions_json.im2txt && + editMode && + !imageCaption?.includes(photoDetail.captions_json.im2txt) && ( +
+ + + + Suggestion + + + ({ + display: "block", + padding: theme.spacing.xs, + borderRadius: theme.radius.xl, + textDecoration: "none", + fontSize: theme.fontSizes.sm, + color: theme.colorScheme === "dark" ? theme.colors.dark[1] : theme.colors.gray[7], + backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[1], + "&:hover": { + backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[3], + }, + })} + onClick={() => { + editor?.commands.setContent(photoDetail.captions_json.im2txt); + }} + > + {photoDetail.captions_json.im2txt} + +
+
+ )} + +
+ + + {editMode && ( + { + dispatch(generatePhotoIm2txtCaption(photoDetail.image_hash)); + }} + disabled={isPublic || (generatingCaptionIm2txt != null && generatingCaptionIm2txt)} + > + + + )} + {!editMode && !isPublic && ( + { + setEditMode(true); + editor?.setEditable(true); + }} + disabled={isPublic || (generatingCaptionIm2txt != null && generatingCaptionIm2txt)} + > + + + )} + +
+ {editMode && ( + + + { + setEditMode(false); + }} + color="red" + > + + + + + { + dispatch(savePhotoCaption(photoDetail.image_hash, imageCaption)); + setEditMode(false); + }} + > + + + + + )} + {photoDetail.captions_json.places365 && ( + + + + {t("lightbox.sidebar.scene")} + + {t("lightbox.sidebar.attributes")} + + {photoDetail.captions_json.places365.attributes.map(nc => ( + { + dispatch(push(`/search/${nc}`)); + }} + > + {nc} + + ))} + + + {t("lightbox.sidebar.categories")} + + {photoDetail.captions_json.places365.categories.map(nc => ( + { + dispatch(push(`/search/${nc}`)); + }} + > + {nc} + + ))} + + + )} + + + ); +} diff --git a/src/components/lightbox/MentionList.tsx b/src/components/lightbox/MentionList.tsx new file mode 100644 index 00000000..e26e3052 --- /dev/null +++ b/src/components/lightbox/MentionList.tsx @@ -0,0 +1,93 @@ +import React, { forwardRef, useEffect, useImperativeHandle, useState } from "react"; + +type Props = { + items: string[]; + command: (params: { id: string }) => void; +}; + +export const MentionList = forwardRef((props: Props, ref) => { + const [selectedIndex, setSelectedIndex] = useState(0); + + const selectItem = index => { + const item = props.items[index]; + + if (item) { + props.command({ id: item }); + } + }; + + const upHandler = () => { + setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length); + }; + + const downHandler = () => { + setSelectedIndex((selectedIndex + 1) % props.items.length); + }; + + const enterHandler = () => { + selectItem(selectedIndex); + }; + + useEffect(() => setSelectedIndex(0), [props.items]); + + useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }) => { + if (event.key === "ArrowUp") { + upHandler(); + return true; + } + + if (event.key === "ArrowDown") { + downHandler(); + return true; + } + + if (event.key === "Enter") { + enterHandler(); + return true; + } + + return false; + }, + })); + + return ( +
+ {props.items.length ? ( + props.items.map((item, index) => ( + + )) + ) : ( +
No result
+ )} +
+ ); +}); diff --git a/src/components/lightbox/Sidebar.tsx b/src/components/lightbox/Sidebar.tsx index 6dfabfc8..50242c4e 100644 --- a/src/components/lightbox/Sidebar.tsx +++ b/src/components/lightbox/Sidebar.tsx @@ -1,45 +1,29 @@ -import { - ActionIcon, - Avatar, - Badge, - Box, - Button, - Group, - Stack, - Text, - Textarea, - Title, - Tooltip, - UnstyledButton, -} from "@mantine/core"; +import { ActionIcon, Avatar, Box, Button, Group, Stack, Text, Title, Tooltip } from "@mantine/core"; import { useViewportSize } from "@mantine/hooks"; import { showNotification } from "@mantine/notifications"; // only needs to be imported once import { IconEdit as Edit, IconMap2 as Map2, - IconNote as Note, IconPhoto as Photo, - IconTags as Tags, IconUserOff as UserOff, IconUsers as Users, - IconWand as Wand, IconX as X, } from "@tabler/icons-react"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import "react-virtualized/styles.css"; import { push } from "redux-first-history"; -import { generatePhotoIm2txtCaption, savePhotoCaption } from "../../actions/photosActions"; import type { Photo as PhotoType } from "../../actions/photosActions.types"; import { api } from "../../api_client/api"; import { serverAddress } from "../../api_client/apiClient"; import { photoDetailsApi } from "../../api_client/photos/photoDetail"; -import { useAppDispatch, useAppSelector } from "../../store/store"; +import { useAppDispatch } from "../../store/store"; import { LocationMap } from "../LocationMap"; import { Tile } from "../Tile"; import { ModalPersonEdit } from "../modals/ModalPersonEdit"; +import { Description } from "./Description"; import { TimestampItem } from "./TimestampItem"; import { VersionComponent } from "./VersionComponent"; @@ -48,17 +32,14 @@ type Props = { photoDetail: PhotoType; closeSidepanel: () => void; }; - export function Sidebar(props: Props) { const { t } = useTranslation(); const dispatch = useAppDispatch(); const [personEditOpen, setPersonEditOpen] = useState(false); const [selectedFaces, setSelectedFaces] = useState([]); - const { generatingCaptionIm2txt } = useAppSelector(store => store.photos); const { photoDetail, isPublic, closeSidepanel } = props; - - const [imageCaption, setImageCaption] = useState(""); const { width } = useViewportSize(); + const SCROLLBAR_WIDTH = 15; let LIGHTBOX_SIDEBAR_WIDTH = 320; @@ -75,15 +56,6 @@ export function Sidebar(props: Props) { dispatch(photoDetailsApi.endpoints.fetchPhotoDetails.initiate(photoDetail.image_hash)).refetch(); }; - useEffect(() => { - if (photoDetail) { - const currentCaption = photoDetail.captions_json.user_caption; - setImageCaption(currentCaption); - } else { - setImageCaption(""); - } - }, [photoDetail]); - if (width < 600) { LIGHTBOX_SIDEBAR_WIDTH = width - SCROLLBAR_WIDTH; } @@ -186,126 +158,8 @@ export function Sidebar(props: Props) { {/* End Item People */} {/* Start Item Caption */} + - - {false && photoDetail.captions_json.im2txt} - -