Created
January 3, 2024 17:59
-
-
Save pablomikel/f6bf79d0af53ec76ce7a1718fe4c0e6e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { useEffect, useState } from "react"; | |
| import { useRouter } from "next/router"; | |
| import { RedirectToSignIn, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; | |
| import * as Dialog from "@radix-ui/react-dialog"; | |
| import isEqual from "deep-eql"; | |
| import { AnimatePresence, motion, useAnimate } from "framer-motion"; | |
| import { useHotkeys } from "react-hotkeys-hook"; | |
| import { | |
| TbBold, | |
| TbCheck, | |
| TbChevronUp, | |
| TbClipboard, | |
| TbClipboardCheck, | |
| TbClipboardX, | |
| TbCloudUpload, | |
| TbFilePlus, | |
| TbFiles, | |
| TbItalic, | |
| TbLoader, | |
| TbPhotoPlus, | |
| TbTrash, | |
| TbTrashX, | |
| TbX, | |
| } from "react-icons/tb"; | |
| import styled from "styled-components"; | |
| import { useSessionStorage } from "usehooks-ts"; | |
| import { allExtensions } from "@poplar/tiptap"; | |
| import type { Editor } from "@poplar/tiptap/core"; | |
| import type { JSONContent } from "@poplar/tiptap/react"; | |
| import { BubbleMenu, EditorContent, useEditor } from "@poplar/tiptap/react"; | |
| import Button, { ButtonStyled } from "~/components/Button"; | |
| import { FolderSelector } from "~/components/FolderSelector"; | |
| import Skeleton from "~/components/Skeleton"; | |
| import { StyledInput } from "~/components/Tabs"; | |
| import Toolbar from "~/components/Toolbar"; | |
| import Version from "~/components/Version"; | |
| import { theme } from "~/styles/theme"; | |
| import { api } from "~/utils/api"; | |
| import extractIdFromParam from "~/utils/extractIdFromParam"; | |
| import jsonContentIsValid from "~/utils/jsonContentIsValid"; | |
| import { useUploadThing } from "~/utils/uploadthing"; | |
| import useDebounce from "~/utils/useDebounce"; | |
| import { Main } from "../.."; | |
| export const EditorContainer = styled.div` | |
| border: 1px solid ${theme.colors.stroke.actionable}; | |
| border-bottom: none; | |
| border-radius: 6px 6px 0px 0px; | |
| width: calc(100% - 32px); | |
| max-width: 800px; | |
| position: relative; | |
| min-height: fill-available; | |
| display: grid; | |
| justify-self: center; | |
| margin: 0px 16px 0 16px; | |
| overflow: hidden; | |
| box-sizing: border-box; | |
| `; | |
| export const SkeletonContainer = styled.div` | |
| position: absolute; | |
| inset: 0; | |
| align-content: start; | |
| z-index: 10; | |
| pointer-events: none; | |
| display: grid; | |
| justify-content: stretch; | |
| align-content: stretch; | |
| ${Skeleton} { | |
| min-height: 100%; | |
| min-width: 100%; | |
| } | |
| `; | |
| export const Container = styled.div` | |
| grid-template-rows: 1fr; | |
| min-height: fill-available; | |
| display: grid; | |
| grid-auto-flow: row; | |
| justify-content: stretch; | |
| position: relative; | |
| `; | |
| const SaveStatus = styled.div` | |
| display: grid; | |
| grid-auto-flow: column; | |
| padding: 0 8px; | |
| gap: 8px; | |
| font-size: 16px; | |
| color: ${theme.colors.text.base}; | |
| justify-self: stretch; | |
| align-items: center; | |
| justify-items: end; | |
| span { | |
| font-size: 14px; | |
| justify-self: start; | |
| } | |
| `; | |
| export const VersionContainer = styled.div` | |
| backdrop-filter: saturate(180%) blur(8px); | |
| :before { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background-color: ${theme.colors.bg.subdued}; | |
| z-index: -1; | |
| opacity: 0.5; | |
| } | |
| `; | |
| const ClipBoardButton = ({ editor }: { editor: Editor }) => { | |
| const [copiedState, setCopiedState] = useState< | |
| "idle" | "success" | "failure" | |
| >("idle"); | |
| useEffect(() => { | |
| if (copiedState === "idle") { | |
| return; | |
| } | |
| const timeout = setTimeout(() => { | |
| setCopiedState("idle"); | |
| }, 1000); | |
| return () => { | |
| clearTimeout(timeout); | |
| }; | |
| }, [copiedState]); | |
| return ( | |
| <Button | |
| onClick={() => { | |
| const { from, to, empty } = editor.state.selection; | |
| if (empty) { | |
| return null; | |
| } | |
| const text = editor.state.doc.textBetween(from, to, " "); | |
| navigator.clipboard | |
| .writeText(text) | |
| .then(() => { | |
| setCopiedState("success"); | |
| }) | |
| .catch(() => { | |
| setCopiedState("failure"); | |
| }); | |
| }} | |
| > | |
| {copiedState === "idle" ? ( | |
| <TbClipboard /> | |
| ) : copiedState === "success" ? ( | |
| <TbClipboardCheck /> | |
| ) : ( | |
| <TbClipboardX /> | |
| )} | |
| </Button> | |
| ); | |
| }; | |
| const EditDocumentPage = () => { | |
| const router = useRouter(); | |
| const { id } = router.query; | |
| const [versionCountRef, animate] = useAnimate<HTMLSpanElement>(); | |
| const auth = useAuth(); | |
| const { isSignedIn } = auth; | |
| const context = api.useContext(); | |
| const [saveStatus, setSaveStatus] = useState< | |
| "saved" | "saving" | "error" | "" | |
| >(""); | |
| const documentId = extractIdFromParam(id); | |
| const [slug, setSlug] = useState<string>(""); | |
| const [slugActive, setSlugActive] = useState<boolean>(false); | |
| const document = api.document.getDocumentById.useQuery( | |
| { id: documentId ?? "" }, | |
| { | |
| enabled: router.isReady && !!documentId && isSignedIn, | |
| onSuccess: (data) => { | |
| if (!data) { | |
| return; | |
| } | |
| setSlug(data.slug ?? ""); | |
| }, | |
| }, | |
| ); | |
| const updateDocument = api.document.updateDocument.useMutation({ | |
| onSuccess: (res) => { | |
| setSaveStatus("saved"); | |
| context.document.getDocumentById.setData({ id: res.id }, () => { | |
| return res; | |
| }); | |
| setTimeout(() => { | |
| setSaveStatus(""); | |
| }, 1000); | |
| }, | |
| }); | |
| const newVersion = api.document.createVersion.useMutation({ | |
| onSuccess: (data) => { | |
| void animate( | |
| versionCountRef.current, | |
| { | |
| scale: [1.3, 1], | |
| }, | |
| { duration: 0.3 }, | |
| ); | |
| if (!documentId) { | |
| return; | |
| } | |
| const oldDocumentById = context.document.getDocumentById.getData({ | |
| id: documentId, | |
| }); | |
| if (!oldDocumentById) { | |
| return; | |
| } | |
| context.document.getDocumentById.setData({ id: documentId }, () => { | |
| return { | |
| ...oldDocumentById, | |
| versions: data.versions, | |
| }; | |
| }); | |
| }, | |
| }); | |
| const deleteDocument = api.document.deleteDocumentById.useMutation({ | |
| onSuccess: async (data) => { | |
| if (!documentId) { | |
| return; | |
| } | |
| /* | |
| const oldDocumentsByCurrentUser = | |
| context.document.getDocumentsByCurrentUser.getData(); | |
| context.document.getDocumentsByCurrentUser.setData(undefined, () => { | |
| return assign(oldDocumentsByCurrentUser, data); | |
| }); | |
| */ | |
| await router.push("/files").then(() => { | |
| context.document.getDocumentById.setData( | |
| { id: documentId }, | |
| (oldData) => { | |
| if (!oldData) { | |
| return; | |
| } | |
| return { | |
| ...oldData, | |
| ...data, | |
| }; | |
| }, | |
| ); | |
| }); | |
| }, | |
| }); | |
| const restoreDocument = api.document.restoreDocumentById.useMutation({ | |
| onSuccess: (data) => { | |
| if (!documentId) { | |
| return; | |
| } | |
| /* | |
| const oldData = context.document.getDocumentsByCurrentUser.getData(); | |
| context.document.getDocumentsByCurrentUser.setData(undefined, () => { | |
| return oldData?.filter((d) => d.id !== documentId); | |
| }); | |
| */ | |
| context.document.getDocumentById.setData({ id: documentId }, () => { | |
| return data; | |
| }); | |
| }, | |
| }); | |
| const [showVersions, setShowVersions] = useState(false); | |
| const updateSlug = api.document.updateDocumentSlug.useMutation({ | |
| onMutate: () => { | |
| setSaveStatus("saving"); | |
| }, | |
| onSuccess: (res) => { | |
| if (!documentId) { | |
| return; | |
| } | |
| context.document.getDocumentById.setData({ id: documentId }, (data) => { | |
| if (!data) { | |
| return; | |
| } | |
| return { | |
| ...data, | |
| slug: res, | |
| }; | |
| }); | |
| setSaveStatus("saved"); | |
| setTimeout(() => { | |
| setSaveStatus(""); | |
| }, 1000); | |
| }, | |
| }); | |
| const debouncedUpdateSlug = useDebounce(async () => { | |
| if (!documentId) { | |
| return null; | |
| } | |
| await updateSlug.mutateAsync({ | |
| documentId: documentId, | |
| slug: slug, | |
| }); | |
| }, 400); | |
| const newAutoSave = useDebounce(async () => { | |
| const content = editor?.getJSON(); | |
| if (!content) { | |
| return; | |
| } | |
| if (!documentId) { | |
| return; | |
| } | |
| if (!document.data) { | |
| return; | |
| } | |
| if (document.data?.deleted) { | |
| return; | |
| } | |
| const serverContent = document.data?.versions?.[0]?.content; | |
| const isValid = jsonContentIsValid(content, allExtensions); | |
| const isChanged = !isEqual(content, serverContent); | |
| if (documentId && isValid && isChanged) { | |
| // if (serverContent) { | |
| // console.log("DIFF", detailedDiff(serverContent, content)); | |
| // } | |
| await updateDocument | |
| .mutateAsync({ id: documentId, content: content }) | |
| .then(async () => { | |
| await context.document.getDocumentById.invalidate({ id: documentId }); | |
| }); | |
| } else { | |
| setSaveStatus("saved"); | |
| setTimeout(() => { | |
| setSaveStatus(""); | |
| }, 1000); | |
| } | |
| }, 400); | |
| const [editorContent, setEditorContent] = useState<JSONContent | null>(null); | |
| const [init, setInit] = useState(false); | |
| const documentContent = document.data?.versions?.[0]?.content; | |
| const editor = useEditor( | |
| { | |
| extensions: allExtensions, | |
| editable: document.data?.deleted ? false : document.data ? true : false, | |
| content: editorContent, | |
| onUpdate: () => { | |
| setSaveStatus("saving"); | |
| newAutoSave(); | |
| }, | |
| }, | |
| [editorContent, document.data?.deleted], | |
| ); | |
| useEffect(() => { | |
| if (documentContent && !editorContent && !init) { | |
| setEditorContent(documentContent); | |
| } | |
| }, [documentContent, editorContent, editor, init]); | |
| useEffect(() => { | |
| if (document.data) { | |
| setInit(true); | |
| } | |
| }, [document.data]); | |
| /* const flowers = api.user.getPreferences.useQuery(undefined, { | |
| staleTime: Infinity, | |
| refetchOnWindowFocus: false, | |
| enabled: isSignedIn, | |
| }).data?.flowers; */ | |
| const [menuOpen, setMenuOpen] = useSessionStorage("editorMenu", false); | |
| useHotkeys("meta+j", () => setMenuOpen(!menuOpen), { | |
| enableOnContentEditable: true, | |
| }); | |
| /* useEffect(() => { | |
| router.events.on('routeChangeStart', () => { | |
| const content = editor?.getJSON(); | |
| if (!content) { | |
| return; | |
| } | |
| if (!documentId) { | |
| return; | |
| } | |
| const isValid = jsonContentIsValid(content, extensions); | |
| if (!isValid) { | |
| return; | |
| } | |
| updateDocument.mutate({ id: documentId, content: content }); | |
| }); | |
| }, [documentId, editor, router.events, updateDocument]); | |
| useBeforeunload(() => { | |
| const content = editor?.getJSON(); | |
| if (!content) { | |
| return; | |
| } | |
| if (!documentId) { | |
| return; | |
| } | |
| const isValid = jsonContentIsValid(content, extensions); | |
| if (!isValid) { | |
| return; | |
| } | |
| updateDocument.mutate({ id: documentId, content: content }); | |
| }); */ | |
| const uploader = useUploadThing("imageUploader", { | |
| onClientUploadComplete: (res) => { | |
| res?.map((r) => { | |
| editor?.chain().focus().setImage({ src: r.url }).run(); | |
| }); | |
| }, | |
| }); | |
| const { startUpload, isUploading } = uploader; | |
| return ( | |
| <> | |
| <SignedOut> | |
| <RedirectToSignIn /> | |
| </SignedOut> | |
| <SignedIn> | |
| {!document.isLoading && !document.data ? ( | |
| <Main> | |
| <p>No data</p> | |
| </Main> | |
| ) : document.isError ? ( | |
| <Main> | |
| <p>Something went wrong</p> | |
| <p>{document.error.message}</p> | |
| </Main> | |
| ) : ( | |
| <> | |
| <ButtonsContainer | |
| as={motion.div} | |
| initial={false} | |
| animate={ | |
| menuOpen | |
| ? { | |
| background: theme.colors.bg.base.toString(), | |
| boxShadow: theme.shadow.subtle.toString(), | |
| borderColor: theme.colors.stroke.default.toString(), | |
| backdropFilter: "saturate(180%) blur(8px)", | |
| } | |
| : { | |
| background: "transparent", | |
| boxShadow: "none", | |
| borderColor: "transparent", | |
| backdropFilter: "none", | |
| } | |
| } | |
| > | |
| <ButtonsContainerBg | |
| initial={false} | |
| animate={ | |
| menuOpen | |
| ? { | |
| opacity: 0.85, | |
| } | |
| : { | |
| opacity: 0, | |
| } | |
| } | |
| /> | |
| <ButtonsSubContainer | |
| $menuOpen={menuOpen} | |
| initial={false} | |
| as={motion.div} | |
| animate={ | |
| menuOpen | |
| ? { | |
| height: "auto", | |
| marginTop: 16, | |
| marginBottom: 12, | |
| opacity: 1, | |
| } | |
| : { height: 0, marginTop: 12, marginBottom: 0, opacity: 0 } | |
| } | |
| > | |
| {document.data?.versions[0]?.active ? ( | |
| document.data.slug ? ( | |
| <p | |
| style={{ | |
| margin: 0, | |
| padding: "4px 8px", | |
| borderRadius: 4, | |
| fontSize: 12, | |
| boxShadow: theme.shadow.subtle.toString(), | |
| background: theme.colors.avatar.bg.toString(), | |
| color: theme.colors.avatar.text.toString(), | |
| border: `1px solid ${theme.colors.avatar.border.toString()}`, | |
| justifySelf: "start", | |
| }} | |
| > | |
| Published | |
| </p> | |
| ) : ( | |
| <p | |
| style={{ | |
| margin: 0, | |
| padding: "4px 8px", | |
| borderRadius: 4, | |
| fontSize: 12, | |
| boxShadow: theme.shadow.subtle.toString(), | |
| background: | |
| theme.colors.button.danger.default.bg.toString(), | |
| color: | |
| theme.colors.button.danger.default.text.toString(), | |
| border: `1px solid ${theme.colors.button.danger.default.border.toString()}`, | |
| justifySelf: "start", | |
| }} | |
| > | |
| Needs slug | |
| </p> | |
| ) | |
| ) : ( | |
| <p | |
| style={{ | |
| margin: 0, | |
| padding: "4px 8px", | |
| borderRadius: 4, | |
| fontSize: 12, | |
| boxShadow: theme.shadow.subtle.toString(), | |
| background: theme.colors.bg.subdued.toString(), | |
| color: theme.colors.text.subdued.toString(), | |
| border: `1px solid ${theme.colors.stroke.default.toString()}`, | |
| justifySelf: "start", | |
| }} | |
| > | |
| Not published | |
| </p> | |
| )} | |
| <AnimatePresence mode="popLayout"> | |
| {document.data?.deleted ? null : saveStatus === "saving" ? ( | |
| <SaveStatus | |
| key={"saving"} | |
| as={motion.div} | |
| animate={{ opacity: [0.5, 0.5, 1, 0.5, 0.5] }} | |
| transition={{ | |
| duration: 2.5, | |
| repeat: Infinity, | |
| repeatType: "loop", | |
| }} | |
| > | |
| <span>Saving...</span> <TbCloudUpload /> | |
| </SaveStatus> | |
| ) : ( | |
| <SaveStatus | |
| key={"saved"} | |
| as={motion.div} | |
| initial={{ scale: 1 }} | |
| animate={{ | |
| opacity: saveStatus === "saved" ? 1 : 0.5, | |
| scale: saveStatus === "saved" ? 1.01 : 1, | |
| }} | |
| transition={{ | |
| duration: 0.2, | |
| }} | |
| > | |
| <span>Saved</span> <TbCheck /> | |
| </SaveStatus> | |
| )} | |
| </AnimatePresence> | |
| <Button | |
| style={{ gridTemplateColumns: "auto 1fr auto" }} | |
| onClick={() => { | |
| if (documentId) { | |
| context.document.getDocumentById.setData( | |
| { id: documentId }, | |
| (data) => { | |
| const content = editor?.getJSON(); | |
| if (!data || !content) { | |
| return; | |
| } | |
| const newVersions = data.versions.map( | |
| (version, i) => { | |
| if (i === 0) { | |
| return { | |
| ...version, | |
| content: content, | |
| }; | |
| } | |
| return version; | |
| }, | |
| ); | |
| return { | |
| ...data, | |
| versions: newVersions, | |
| }; | |
| }, | |
| ); | |
| setShowVersions(true); | |
| } | |
| }} | |
| disabled={ | |
| !( | |
| document.data?.versions && | |
| document.data.versions.length > 0 | |
| ) | |
| } | |
| > | |
| <TbFiles /> | |
| <span> | |
| <span | |
| style={{ | |
| position: "relative", | |
| }} | |
| > | |
| <span> | |
| {document?.data?.versions.length && | |
| document.data.versions.length > 1 ? ( | |
| <span | |
| style={{ | |
| display: "inline-block", | |
| fontVariantNumeric: "normal", | |
| }} | |
| ref={versionCountRef} | |
| > | |
| {(document.data.versions.length - 1).toString()} | |
| </span> | |
| ) : ( | |
| "No" | |
| )} | |
| </span> | |
| </span>{" "} | |
| snapshot | |
| {document.data?.versions.length === 0 || | |
| (document.data?.versions.length && | |
| document.data?.versions.length === 2) | |
| ? "" | |
| : "s"} | |
| </span> | |
| {document.data?.versions.find( | |
| (version) => version.active, | |
| ) && ( | |
| <div | |
| style={{ | |
| width: 8, | |
| height: 8, | |
| borderRadius: 4, | |
| background: theme.colors.avatar.border.toString(), | |
| justifySelf: "end", | |
| }} | |
| /> | |
| )} | |
| </Button> | |
| {document.data?.deleted ? null : ( | |
| <> | |
| <Button | |
| onClick={() => { | |
| documentId && newVersion.mutate({ id: documentId }); | |
| }} | |
| disabled={ | |
| isEqual( | |
| editor?.getJSON(), | |
| document.data?.versions?.[1]?.content, | |
| ) || | |
| !documentId || | |
| saveStatus === "saving" || | |
| !document.data?.versions?.[0]?.content | |
| } | |
| loading={newVersion.isLoading} | |
| > | |
| <TbFilePlus /> <span>Snapshot</span> | |
| </Button> | |
| </> | |
| )} | |
| {documentId && document.data && ( | |
| <FolderSelector | |
| defaultValue={document.data.folderId ?? "no-folder"} | |
| documentId={documentId} | |
| /> | |
| )} | |
| <div style={{ position: "relative" }}> | |
| <StyledInput | |
| style={{ | |
| position: "absolute", | |
| inset: 0, | |
| zIndex: 2, | |
| pointerEvents: "none", | |
| opacity: slugActive ? 0 : 1, | |
| }} | |
| placeholder="Slug..." | |
| value={encodeURI(slug)} | |
| /> | |
| <StyledInput | |
| style={{ position: "relative", zIndex: 1 }} | |
| value={slug} | |
| onBlur={() => setSlugActive(false)} | |
| onFocus={() => setSlugActive(true)} | |
| onChange={(e) => { | |
| setSlug(e.target.value); | |
| debouncedUpdateSlug(); | |
| }} | |
| /> | |
| </div> | |
| <div | |
| style={{ | |
| position: "relative", | |
| justifySelf: "stretch", | |
| display: "grid", | |
| }} | |
| > | |
| <ButtonStyled style={{ justifyContent: "start" }}> | |
| {isUploading ? ( | |
| <> | |
| <Spinning> | |
| <TbLoader /> | |
| </Spinning> | |
| Uploading... | |
| </> | |
| ) : ( | |
| <> | |
| <TbPhotoPlus /> | |
| Add image | |
| </> | |
| )} | |
| </ButtonStyled> | |
| <input | |
| style={{ | |
| position: "absolute", | |
| inset: 0, | |
| opacity: 0, | |
| width: "100%", | |
| height: "100%", | |
| cursor: "pointer", | |
| }} | |
| type="file" | |
| onChange={async (e) => { | |
| const file = e.target.files?.[0]; | |
| if (!file) return; | |
| await startUpload([file]); | |
| }} | |
| /> | |
| </div> | |
| {document.data?.deleted ? ( | |
| <Button | |
| onClick={() => | |
| documentId && restoreDocument.mutate({ id: documentId }) | |
| } | |
| > | |
| <TbTrashX /> <span>Restore</span> | |
| </Button> | |
| ) : ( | |
| <Button | |
| variant="danger" | |
| onClick={() => | |
| documentId && deleteDocument.mutate({ id: documentId }) | |
| } | |
| > | |
| <TbTrash /> <span>Delete</span> | |
| </Button> | |
| )} | |
| </ButtonsSubContainer> | |
| <Button onClick={() => setMenuOpen(!menuOpen)}> | |
| <TbChevronUp | |
| style={{ | |
| width: 14, | |
| height: 14, | |
| rotate: menuOpen ? "-180deg" : "0deg", | |
| transition: "rotate 0.2s ease-in-out", | |
| }} | |
| /> | |
| </Button> | |
| </ButtonsContainer> | |
| <Container> | |
| <EditorContainer | |
| // as={motion.div} | |
| // initial={{ y: 4 }} | |
| // animate={{ y: 0 }} | |
| > | |
| <> | |
| {document.isLoading && ( | |
| <SkeletonContainer> | |
| <Skeleton /> | |
| </SkeletonContainer> | |
| )} | |
| </> | |
| {editor && ( | |
| <StyledBubbleMenu | |
| editor={editor} | |
| tippyOptions={{ duration: 100 }} | |
| shouldShow={(props) => | |
| (props.editor.isActive("image") ? false : true) && | |
| !props.editor.state.selection.empty | |
| } | |
| > | |
| <Button | |
| onClick={() => editor.chain().focus().toggleBold().run()} | |
| active={editor.isActive("bold")} | |
| > | |
| <TbBold /> | |
| </Button> | |
| <Button | |
| onClick={() => | |
| editor.chain().focus().toggleItalic().run() | |
| } | |
| active={editor.isActive("italic")} | |
| > | |
| <TbItalic /> | |
| </Button> | |
| <ClipBoardButton editor={editor} /> | |
| </StyledBubbleMenu> | |
| )} | |
| {/* | |
| {editor && ( | |
| <StyledFloatingMenu | |
| editor={editor} | |
| tippyOptions={{ duration: 100 }} | |
| > | |
| <Button onClick={addImage}> | |
| <TbPhotoPlus /> | |
| </Button> | |
| </StyledFloatingMenu> | |
| )} | |
| */} | |
| <EditorContent | |
| editor={editor} | |
| style={{ | |
| display: "flex", | |
| minHeight: "fill-available", | |
| }} | |
| /> | |
| </EditorContainer> | |
| <Dialog.Root open={showVersions} onOpenChange={setShowVersions}> | |
| <Dialog.Portal> | |
| <Dialog.Overlay style={{ position: "absolute", inset: 0 }} /> | |
| <Dialog.Content | |
| asChild | |
| onOpenAutoFocus={(event) => event.preventDefault()} | |
| > | |
| <VersionContainer | |
| style={{ | |
| display: "grid", | |
| gridTemplateRows: "1fr", | |
| position: "fixed", | |
| inset: 0, | |
| alignContent: "stretch", | |
| alignItems: "stretch", | |
| overflow: "hidden", | |
| zIndex: 100000, | |
| }} | |
| > | |
| <Toolbar | |
| showBorder={false} | |
| slot={{ | |
| left: ( | |
| <Button onClick={() => setShowVersions(false)}> | |
| <TbX /> | |
| <span>Close</span> | |
| </Button> | |
| ), | |
| }} | |
| /> | |
| <div | |
| style={{ | |
| display: "grid", | |
| gridAutoFlow: "column", | |
| overflowX: "auto", | |
| alignContent: "stretch", | |
| paddingLeft: | |
| "clamp(48px, calc(50vw - 332px), calc(50vw - 332px))", | |
| paddingRight: | |
| "clamp(48px, calc(50vw - 332px), calc(50vw - 332px))", | |
| scrollSnapType: "x mandatory", | |
| scrollPaddingLeft: | |
| "clamp(48px, calc(50vw - 332px), calc(50vw - 332px))", | |
| paddingTop: 90, | |
| }} | |
| > | |
| <AnimatePresence> | |
| {document.data?.versions.map((version, i) => { | |
| return ( | |
| <Version | |
| key={version.id} | |
| version={version} | |
| i={i} | |
| documentId={documentId} | |
| /> | |
| ); | |
| })} | |
| </AnimatePresence> | |
| </div> | |
| </VersionContainer> | |
| </Dialog.Content> | |
| </Dialog.Portal> | |
| </Dialog.Root> | |
| {/* {!!flowers && ( | |
| <div | |
| style={{ | |
| position: "fixed", | |
| bottom: "0", | |
| left: "0", | |
| right: "0", | |
| display: "flex", | |
| justifyContent: "center", | |
| alignContent: "end", | |
| zIndex: -1, | |
| }} | |
| > | |
| <Flowers | |
| numOfFlowers={ | |
| ( | |
| editor?.storage.characterCount as CharacterCountStorage | |
| )?.words() ?? 0 | |
| } | |
| salt={documentId ?? ""} | |
| /> | |
| </div> | |
| )} */} | |
| </Container> | |
| </> | |
| )} | |
| </SignedIn> | |
| </> | |
| ); | |
| }; | |
| export default EditDocumentPage; | |
| const StyledBubbleMenu = styled(BubbleMenu)` | |
| display: grid; | |
| grid-auto-flow: column; | |
| gap: 8px; | |
| button { | |
| font-size: 16px; | |
| padding: 4px 4px; | |
| border-radius: 4px; | |
| outline-width: 2px; | |
| height: unset; | |
| } | |
| `; | |
| /* const StyledFloatingMenu = styled(FloatingMenu)` | |
| button { | |
| font-size: 16px; | |
| padding: 4px 4px; | |
| border-radius: 4px; | |
| outline-width: 2px; | |
| height: unset; | |
| } | |
| `; */ | |
| export const ButtonsContainer = styled.div` | |
| display: grid; | |
| grid-auto-flow: row; | |
| justify-content: end; | |
| justify-items: end; | |
| align-items: center; | |
| justify-self: center; | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| z-index: 100; | |
| padding: 12px; | |
| padding-top: 0; | |
| box-shadow: ${theme.shadow.subtle}; | |
| border-radius: 12px; | |
| border: 1px solid ${theme.colors.stroke.default}; | |
| background: transparent; | |
| backdrop-filter: saturate(180%) blur(8px); | |
| overflow: hidden; | |
| box-sizing: border-box; | |
| `; | |
| export const ButtonsContainerBg = styled(motion.div)` | |
| position: absolute; | |
| inset: 0; | |
| background-color: ${theme.colors.bg.base}; | |
| opacity: 0.85; | |
| z-index: 0; | |
| `; | |
| export const ButtonsSubContainer = styled.div<{ $menuOpen: boolean }>` | |
| display: grid; | |
| grid-auto-flow: row; | |
| justify-content: stretch; | |
| justify-items: stretch; | |
| align-items: end; | |
| align-content: end; | |
| gap: 16px; | |
| box-sizing: border-box; | |
| pointer-events: ${(props) => (props.$menuOpen ? "all" : "none")}; | |
| position: relative; | |
| z-index: 1; | |
| button { | |
| justify-content: start; | |
| } | |
| `; | |
| const Spinning = styled.div` | |
| display: flex; | |
| @-webkit-keyframes rotating /* Safari and Chrome */ { | |
| from { | |
| -webkit-transform: rotate(0deg); | |
| -o-transform: rotate(0deg); | |
| transform: rotate(0deg); | |
| } | |
| to { | |
| -webkit-transform: rotate(360deg); | |
| -o-transform: rotate(360deg); | |
| transform: rotate(360deg); | |
| } | |
| } | |
| @keyframes rotating { | |
| from { | |
| -ms-transform: rotate(0deg); | |
| -moz-transform: rotate(0deg); | |
| -webkit-transform: rotate(0deg); | |
| -o-transform: rotate(0deg); | |
| transform: rotate(0deg); | |
| } | |
| to { | |
| -ms-transform: rotate(360deg); | |
| -moz-transform: rotate(360deg); | |
| -webkit-transform: rotate(360deg); | |
| -o-transform: rotate(360deg); | |
| transform: rotate(360deg); | |
| } | |
| } | |
| animation: rotating 2s linear infinite; | |
| `; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment