Normal tooltip | Fixed quill redactor

This commit is contained in:
Max 2025-02-05 11:34:33 +03:00
parent 6d6babc305
commit 6a22797610
17 changed files with 273 additions and 138 deletions

View File

@ -1,12 +1,12 @@
{
"name": "enshi",
"version": "0.1.7",
"version": "0.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "enshi",
"version": "0.1.7",
"version": "0.1.8",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-form": "^0.1.0",
@ -18,6 +18,7 @@
"@radix-ui/themes": "^3.1.3",
"@tanstack/react-query": "^5.55.0",
"@tanstack/react-query-devtools": "^5.61.0",
"@types/quill": "^2.0.14",
"axios": "^1.7.7",
"html-react-parser": "^5.1.16",
"i18n": "^0.15.1",
@ -28,7 +29,7 @@
"jotai": "^2.9.3",
"jotai-immer": "^0.4.1",
"primereact": "^10.8.2",
"quill": "^2.0.2",
"quill": "^2.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.1",
@ -3422,12 +3423,13 @@
"license": "MIT"
},
"node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-2.0.14.tgz",
"integrity": "sha512-zvoXCRnc2Dl8g+7/9VSAmRWPN6oH+MVhTPizmCR+GJCITplZ5VRVzMs4+a/nOE3yzNwEZqylJJrMB07bwbM1/g==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
"parchment": "^1.1.2",
"quill-delta": "^5.1.0"
}
},
"node_modules/@types/quill/node_modules/parchment": {
@ -6409,9 +6411,9 @@
"license": "MIT"
},
"node_modules/quill": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
"integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
"license": "BSD-3-Clause",
"dependencies": {
"eventemitter3": "^5.0.1",
@ -6511,6 +6513,15 @@
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/react-quill/node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
}
},
"node_modules/react-quill/node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",

View File

@ -21,6 +21,7 @@
"@radix-ui/themes": "^3.1.3",
"@tanstack/react-query": "^5.55.0",
"@tanstack/react-query-devtools": "^5.61.0",
"@types/quill": "^2.0.14",
"axios": "^1.7.7",
"html-react-parser": "^5.1.16",
"i18n": "^0.15.1",
@ -31,7 +32,7 @@
"jotai": "^2.9.3",
"jotai-immer": "^0.4.1",
"primereact": "^10.8.2",
"quill": "^2.0.2",
"quill": "^2.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.1",

View File

@ -6,3 +6,8 @@ export type GetRandomPostsRow = {
// created_at: Date;
}
export type TPostData = {
title: string;
content: string;
};

View File

@ -426,6 +426,16 @@
content: '';
display: table;
}
.ql-toolbar {
position: relative;
width: 100%;
}
.ql-container {
overflow-y: auto;
}
.ql-snow.ql-toolbar button,
.ql-snow .ql-toolbar button {
background: none;

View File

@ -1,23 +1,28 @@
import { Theme, ThemePanel } from "@radix-ui/themes";
import { Theme } from "@radix-ui/themes";
import "@radix-ui/themes/styles.css";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import "axios";
import { useAtomValue } from "jotai";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import queryClient from "./api/QueryClient/QueryClient";
import "./App.css";
import { themeAtom } from "./AtomStore/AtomStore";
import ToastProvider from "./Components/ToastProvider/ToastProvider";
import { routes } from "./routes/routes";
const router = createBrowserRouter(routes);
export default function App() {
const theme = useAtomValue(themeAtom);
return (
<Theme className="h-fit" accentColor="sky" grayColor="slate" appearance="dark">
<Theme className="h-fit" accentColor="sky" grayColor="slate" appearance={theme}>
<ToastProvider>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<ThemePanel />
{/* <ThemePanel /> */}
<ReactQueryDevtools/>
</QueryClientProvider>
</ToastProvider>

View File

@ -1,5 +1,6 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { TPostData } from "../@types/PostTypes";
import { TUser } from "../@types/UserType";
export const userAtom = atom<TUser>();
@ -7,10 +8,18 @@ export const userAtom = atom<TUser>();
export const postCreationAtom = atom<string>();
export const postCreationTitleAtom = atom<string>();
type TPostData = {
title: string;
content: string;
};
export const themeAtom = atomWithStorage<"light" | "dark">(
"theme",
"light",
{
getItem: (key) => localStorage.getItem(key) as any,
setItem: (key, value) => localStorage.setItem(key, value as any),
removeItem: (key) => localStorage.removeItem(key),
},
{
getOnInit: true,
}
);
export const storagePostAtom = atomWithStorage<TPostData>(
"draft-post",

View File

@ -1,7 +1,5 @@
import Sources from "quill";
import Quill, { Delta } from "quill/core";
import { forwardRef, useEffect, useRef, useState } from "react";
import ReactQuill from "react-quill";
import { Delta } from "quill/core";
import { forwardRef } from "react";
type TEditor = {
readOnly?: boolean;
@ -30,61 +28,55 @@ const modules = {
* @param onChange - function that accepts Delta element
*/
const Editor = forwardRef((props: TEditor) => {
const editor = useRef(null);
const [quill, setQuill] = useState<Quill | null>(null);
const [value, setValue] = useState(new Delta());
// const editor = useRef(null);
// const [quill, setQuill] = useState<Quill | null>(null);
// const [value, setValue] = useState(new Delta());
const [loaded, setLoaded] = useState(false);
// const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (editor.current) {
//@ts-ignore
const temp = editor.current.getEditor() as Quill;
setQuill(temp);
}
return () => {
setQuill(null);
};
}, [editor.current]);
// useEffect(() => {
// if (editor.current) {
// //@ts-ignore
// const temp = editor.current.getEditor() as Quill;
// setQuill(temp);
// }
// return () => {
// setQuill(null);
// };
// }, [editor.current]);
useEffect(() => {
const quill = new Quill(document.createElement("div"));
const t = quill.clipboard.convert({
html: props.defaultValue as string,
}) as Delta;
// useEffect(() => {
// const quill = new Quill(document.createElement("div"));
// console.log(`AMOOOGUS`, props.defaultValue);
if (!loaded) {
setValue(t);
// const t = quill.clipboard.convert({
// html: props.defaultValue as string,
// }) as Delta;
console.log(t);
}
// if (!loaded) {
// setValue(t);
// console.log(t);
// }
setLoaded(true);
}, [props.defaultValue]);
// setLoaded(true);
// }, [props.defaultValue]);
const changeHandler = (
val: string,
_changeDelta: Delta,
_source: Sources,
_editor: ReactQuill.UnprivilegedEditor
) => {
console.log(val);
console.log(JSON.stringify(quill?.getContents().ops, null, 2));
let fullDelta = quill?.getContents();
if (props.onChange) props.onChange(val || "");
if (loaded) setValue(fullDelta || new Delta());
};
// const changeHandler = (
// val: string,
// _changeDelta: Delta,
// _source: Sources,
// _editor: ReactQuill.UnprivilegedEditor
// ) => {
// console.log(val);
// console.log(JSON.stringify(quill?.getContents().ops, null, 2));
// let fullDelta = quill?.getContents();
// if (props.onChange) props.onChange(val || "");
// if (loaded) setValue(fullDelta || new Delta());
// };
return (
<div className="text-editor">
<ReactQuill
value={value}
ref={editor}
modules={modules}
onChange={changeHandler}
theme="snow"
placeholder="Type your thoughts here..."
/>
<div className="text-editor h-[400px]">
DEPRECATED
</div>
);
});

View File

@ -0,0 +1,85 @@
import { Delta, default as Quill, default as Sources } from "quill";
import "quill/dist/quill.snow.css"; // make sure to import Quill's CSS
import { useEffect, useRef } from "react";
type TEditor = {
readOnly?: boolean;
defaultValue?: string | Delta;
onChange?: (html: string) => void;
onSelectionChange?: (range: any) => void;
};
const modules = {
toolbar: [
[{ header: [1, 2, 3, 4, 5, false] }],
["bold", "italic", "underline", "strike", "blockquote", "span-wrapper"],
[
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
],
["link", "image"],
["clean"],
[{ align: [] }],
],
};
const TrueEditor = (props: TEditor) => {
const containerRef = useRef<HTMLDivElement>(null);
const quillRef = useRef<Quill | null>(null);
useEffect(() => {
if (containerRef.current) {
quillRef.current = new Quill(containerRef.current, {
modules,
theme: "snow",
readOnly: props.readOnly || false,
placeholder: "Type your thoughts here...",
});
if (props.defaultValue) {
if (typeof props.defaultValue === "string") {
const delta = quillRef.current.clipboard.convert({ html: props.defaultValue });
quillRef.current.setContents(delta, "silent");
} else {
quillRef.current.setContents(props.defaultValue, "silent");
}
}
quillRef.current.on(
"text-change",
(_delta: Delta, _oldDelta: Delta, _source: Sources) => {
if (props.onChange) {
const html =
containerRef.current?.querySelector(".ql-editor")?.innerHTML ||
"";
props.onChange(html);
}
}
);
if (props.onSelectionChange) {
quillRef.current.on("selection-change", (range, _oldRange, _source) => {
if(props.onSelectionChange) props.onSelectionChange(range);
});
}
}
return () => {
if (quillRef.current) {
quillRef.current.off("text-change");
quillRef.current.off("selection-change");
quillRef.current = null;
}
};
}, []);
return (
<div className="flex flex-col w-full mx-auto overflow-hidden text-editor flex-grow-1 max-w-pc-width">
<div ref={containerRef} />
</div>
);
};
export default TrueEditor;

View File

@ -1,4 +1,4 @@
import { PlusIcon } from "@radix-ui/react-icons";
import { Pencil1Icon } from "@radix-ui/react-icons";
import { Button, Text } from "@radix-ui/themes";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@ -8,8 +8,8 @@ export default function CreatePostButton() {
return (
<Link to={"/create"}>
<Button variant="ghost" className="h-full">
<PlusIcon />
<Button variant="ghost" className="items-center h-full px-[6px] pr-[8px] py-0 my-auto m-0">
<Pencil1Icon />
<Text>{t("createPost")}</Text>
</Button>
</Link>

View File

@ -1,11 +1,13 @@
import CreatePostButton from "./CreatePostButton/CreatePostButton";
import ThemeChangeButton from "./ThemeChangeButton/ThemeChangeButton";
import UserButton from "./UserButton/UserButton";
export default function RightButtonBar() {
return (
<div className='flex flex-row justify-end flex-1 gap-4'>
<div className='flex flex-row items-center justify-end flex-1 gap-2'>
<CreatePostButton />
<ThemeChangeButton />
<UserButton />
</div>
)

View File

@ -0,0 +1,23 @@
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
import { IconButton } from "@radix-ui/themes";
import { useAtom } from "jotai";
import { themeAtom } from "../../../../AtomStore/AtomStore";
export default function ThemeChangeButton() {
const [theme, setTheme] = useAtom(themeAtom);
const toggleTheme = () => {
if(theme === 'light') {
setTheme('dark')
} else {
setTheme('light')
}
}
return (
<IconButton onClick={toggleTheme} className="mx-0 my-auto rounded-full p-[8px]" variant="ghost">
{theme === 'light' ? <SunIcon /> : <MoonIcon />}
</IconButton>
)
}

View File

@ -1,5 +1,5 @@
import { LaptopIcon, PersonIcon } from "@radix-ui/react-icons";
import { DropdownMenu, Flex, IconButton, Text } from "@radix-ui/themes";
import { DropdownMenu, Flex, IconButton, Text, Tooltip } from "@radix-ui/themes";
import { Icon } from "@radix-ui/themes/dist/esm/components/callout.js";
import { useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";
@ -18,9 +18,15 @@ export default function UserButton() {
<div className="">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton className="cursor-pointer">
<Tooltip content={"User menu"} className="w-fit">
<IconButton
size={"2"}
className="items-center my-auto rounded-full"
variant="ghost"
>
<PersonIcon />
</IconButton>
</Tooltip>
</DropdownMenu.Trigger>
<DropdownMenu.Content className="w-fit">

View File

@ -1,28 +0,0 @@
import * as RadixTooltip from "@radix-ui/react-tooltip";
import { Card, Text, Theme } from "@radix-ui/themes";
type TTooltipProps = {
text: string;
} & React.PropsWithChildren;
export default function Tooltip(props: TTooltipProps) {
return (
<RadixTooltip.Provider>
<RadixTooltip.Root>
<RadixTooltip.Trigger>{props.children}</RadixTooltip.Trigger>
<RadixTooltip.Portal>
<RadixTooltip.Content side="top">
<RadixTooltip.Content>
<Theme panelBackground="translucent">
<Card className="p-1 -translate-y-1 w-fit h-fit animate-appearTooltip">
<Text>{props.text}</Text>
</Card>
</Theme>
</RadixTooltip.Content>
</RadixTooltip.Content>
</RadixTooltip.Portal>
</RadixTooltip.Root>
</RadixTooltip.Provider>
);
}

View File

@ -1,10 +1,10 @@
import { Box, Container, Flex, ScrollArea } from "@radix-ui/themes";
import { Box, Container, Flex } from "@radix-ui/themes";
import { useAtom, useSetAtom } from "jotai";
import {
postCreationAtom,
postCreationTitleAtom,
} from "../../AtomStore/AtomStore";
import Editor from "../../Components/Editor/Editor";
import TrueEditor from "../../Components/Editor/TrueEditor";
import SubmitPostButton from "./SubmitPostButton/SubmitPostButton";
export default function PostCreatorPage() {
@ -14,7 +14,11 @@ export default function PostCreatorPage() {
return (
<>
<Box className="flex flex-col flex-1 overflow-hidden">
<Flex gap={"4"} direction={"column"} className="justify-start overflow-hidden">
<Flex
gap={"4"}
direction={"column"}
className="justify-start overflow-hidden"
>
<Container>
<input
placeholder={"Post title"}
@ -28,11 +32,7 @@ export default function PostCreatorPage() {
/>
</Container>
<ScrollArea>
<Container>
<Editor onChange={setContentValue} />
</Container>
</ScrollArea>
<TrueEditor onChange={setContentValue} />
<Box className="flex justify-center flex-[1] mb-4">
<SubmitPostButton className="text-2xl rounded-full w-52" />

View File

@ -2,8 +2,8 @@ import { Box, Container, Flex, Spinner } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { axiosLocalhost } from "../../../api/axios/axios";
import Editor from "../../../Components/Editor/Editor";
import { axiosLocalhost } from "../../api/axios/axios";
import TrueEditor from "../../Components/Editor/TrueEditor";
import SubmitChangesButton from "./SubmitChangesButton/SubmitChangesButton";
export default function PostRedactor() {
@ -12,7 +12,7 @@ export default function PostRedactor() {
const queryParams = useParams();
const { isPending } = useQuery({
const { isLoading } = useQuery({
queryKey: ["changePostKey", queryParams.postId],
queryFn: async () => {
try {
@ -31,16 +31,20 @@ export default function PostRedactor() {
}
},
gcTime: 0,
refetchOnMount: true
refetchOnMount: true,
});
return (
<>
<Box className="flex flex-col flex-1">
<Flex gap={"4"} direction={"column"} className="flex-[1]">
<Container className="flex-[1]">
<Box className="flex flex-col flex-1 overflow-hidden">
<Flex
gap={"4"}
direction={"column"}
className="overflow-hidden"
>
<Container className="">
<input
disabled={isPending}
disabled={isLoading}
placeholder={"Post title"}
className="mb-2 border-0 border-b-[1px]
outline-none w-full border-b-gray-400
@ -52,7 +56,7 @@ export default function PostRedactor() {
/>
</Container>
<Container className="overflow-y-auto flex-grow-[100]">
{/* <Container className="overflow-hidden flex-grow-[100]">
{isPending ? (
<Spinner />
) : (
@ -61,13 +65,23 @@ export default function PostRedactor() {
onChange={setContentValue}
/>
)}
</Container>
</Container> */}
<Box className="flex justify-center flex-[1] mb-4">
{isLoading ? (
<Spinner />
) : (
<TrueEditor
defaultValue={contentValue}
onChange={setContentValue}
/>
)}
<Box className="flex justify-center flex-[1] mb-4 ">
<SubmitChangesButton
contentValue={contentValue}
titleValue={titleValue}
className="text-2xl rounded-full w-52" />
className="text-2xl rounded-full w-52"
/>
</Box>
</Flex>
</Box>

View File

@ -3,8 +3,8 @@ import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { axiosLocalhost } from "../../../../api/axios/axios";
import useToast from "../../../../hooks/useToast";
import { axiosLocalhost } from "../../../api/axios/axios";
import useToast from "../../../hooks/useToast";
type TSubmitChangesButton = {
className: string;

View File

@ -11,9 +11,9 @@ import ProfilePage from "../layout/ProfilePage/ProfilePage";
import AuthPageWrapper from "../Pages/AuthPageWrapper/AuthPageWrapper";
import BlogPage from "../Pages/BlogPage/BlogPage";
import LoginPage from "../Pages/LoginRegisterPage/LoginPage/LoginPage";
import PostRedactor from "../Pages/LoginRegisterPage/PostRedactor/PostRedactor";
import RegisterPage from "../Pages/LoginRegisterPage/RegisterPage/RegisterPage";
import PostCreatorPage from "../Pages/PostCreatorPage/PostCreatorPage";
import PostRedactor from "../Pages/PostRedactor/PostRedactor";
import RandomPostsPage from "../Pages/RandomPostsPage/RandomPostsPage";
import UserBlogsPage from "../Pages/UserBlogsPage/UserBlogsPage";
import UserPostsPage from "../Pages/UserPostsPage/UserPostsPage";