mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-20 23:04:24 +01:00
feat: InsertImageModal, img cmd in TextEditor, Make AddComment Controlled
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import InsertImageModal from './InsertImageModal';
|
||||
|
||||
import { ModalsDecorator } from 'src/utils/storybook/decorators';
|
||||
|
||||
export default {
|
||||
title: 'Shared/TextEditor/Insert Image Modal',
|
||||
component: InsertImageModal,
|
||||
|
||||
decorators: [ModalsDecorator]
|
||||
} as ComponentMeta<typeof InsertImageModal>;
|
||||
|
||||
const Template: ComponentStory<typeof InsertImageModal> = (args) => <InsertImageModal {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { FormEvent, useState } from 'react'
|
||||
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
|
||||
import { motion } from 'framer-motion'
|
||||
import { IoClose } from 'react-icons/io5'
|
||||
import Button from 'src/Components/Button/Button'
|
||||
|
||||
interface Props extends ModalCard {
|
||||
onInsert: (img: { src: string, alt?: string }) => void
|
||||
}
|
||||
|
||||
export default function InsertImageModal({ onClose, direction, onInsert, ...props }: Props) {
|
||||
|
||||
const [urlInput, setUrlInput] = useState("")
|
||||
const [altInput, setAltInput] = useState("")
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (urlInput.length > 10) {
|
||||
onInsert({ src: urlInput, alt: altInput })
|
||||
onClose?.();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
custom={direction}
|
||||
variants={modalCardVariants}
|
||||
initial='initial'
|
||||
animate="animate"
|
||||
exit='exit'
|
||||
className="modal-card max-w-[660px] p-24 rounded-xl relative"
|
||||
>
|
||||
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
|
||||
<h2 className='text-h5 font-bold'>Add Image</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid md:grid-cols-3 gap-16 mt-32">
|
||||
<div className='md:col-span-2'>
|
||||
<p className="text-body5">
|
||||
Image URL
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
value={urlInput}
|
||||
onChange={e => setUrlInput(e.target.value)}
|
||||
placeholder='https://images.com/my-image'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-body5">
|
||||
Alt Text
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
value={altInput}
|
||||
onChange={e => setAltInput(e.target.value)}
|
||||
placeholder=''
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-16 justify-end mt-32">
|
||||
<Button onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='submit' color='primary' >
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { WithModals } from 'src/utils/storybook/decorators';
|
||||
|
||||
import TextEditor from './TextEditor';
|
||||
|
||||
export default {
|
||||
title: 'Shared/TextEditor',
|
||||
decorators: [WithModals],
|
||||
component: TextEditor,
|
||||
|
||||
} as ComponentMeta<typeof TextEditor>;
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function TextEditor({ placeholder, initialContent }: Props) {
|
||||
new CodeBlockExtension({
|
||||
supportedLanguages: [javascript, typescript]
|
||||
}),
|
||||
new ImageExtension({ enableResizing: true }),
|
||||
new ImageExtension(),
|
||||
// new TrailingNodeExtension(),
|
||||
// new TableExtension(),
|
||||
new MarkdownExtension({ copyAsMarkdown: false }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useActive, useChainedCommands, useCommands } from '@remirror/react';
|
||||
import { FiBold, FiItalic, FiType, FiUnderline, FiAlignCenter, FiAlignLeft, FiAlignRight, FiCode } from 'react-icons/fi'
|
||||
import { FaListOl, FaListUl, FaUndo, FaRedo } from 'react-icons/fa'
|
||||
import { FaListOl, FaListUl, FaUndo, FaRedo, FaImage } from 'react-icons/fa'
|
||||
|
||||
import {
|
||||
Menu,
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
import '@szhsin/react-menu/dist/index.css';
|
||||
import '@szhsin/react-menu/dist/transitions/slide.css';
|
||||
import { BiCodeCurly } from 'react-icons/bi';
|
||||
import { useAppDispatch } from 'src/utils/hooks';
|
||||
import { openModal } from 'src/redux/features/modals.slice';
|
||||
|
||||
interface Props {
|
||||
cmd: Command
|
||||
@@ -32,6 +34,7 @@ export default function ToolButton({ cmd: _cmd, classes }: Props) {
|
||||
|
||||
const commands = useCommands();
|
||||
const active = useActive();
|
||||
const dispatch = useAppDispatch()
|
||||
// const chain = useChainedCommands();
|
||||
|
||||
// commands.toggleCo
|
||||
@@ -71,6 +74,38 @@ export default function ToolButton({ cmd: _cmd, classes }: Props) {
|
||||
}
|
||||
|
||||
|
||||
if (_cmd === 'img') {
|
||||
const { activeCmd, cmd, tip, Icon } = cmdToBtn[_cmd];
|
||||
const onClick = () => {
|
||||
dispatch(openModal({
|
||||
Modal: "InsertImageModal",
|
||||
props: {
|
||||
onInsert: ({ src, alt }) => {
|
||||
commands.insertImage({
|
||||
src,
|
||||
alt,
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
data-tip={tip}
|
||||
className={`
|
||||
${buttonClasses}
|
||||
${(activeCmd && active[activeCmd]()) && activeClasses}
|
||||
${commands[cmd].enabled({ src: "" }) ? enabledClasses : disabledClasses}
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon className={iconClasses} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
if (isCommand(_cmd)) {
|
||||
const { activeCmd, cmd, tip, Icon } = cmdToBtn[_cmd]
|
||||
@@ -176,6 +211,12 @@ const cmdToBtn = {
|
||||
tip: "Code Block",
|
||||
Icon: BiCodeCurly,
|
||||
},
|
||||
img: {
|
||||
cmd: 'insertImage',
|
||||
activeCmd: 'image',
|
||||
tip: "Insert Image",
|
||||
Icon: FaImage,
|
||||
},
|
||||
|
||||
|
||||
} as const
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Toolbar() {
|
||||
|
||||
|
||||
return (
|
||||
<div className='flex gap-24 bg-gray-100'>
|
||||
<div className='flex flex-wrap gap-24 bg-gray-100'>
|
||||
<div className="flex">
|
||||
<ToolButton cmd='heading' />
|
||||
<ToolButton cmd='bold' />
|
||||
@@ -26,6 +26,7 @@ export default function Toolbar() {
|
||||
<ToolButton cmd='rightAlign' />
|
||||
<ToolButton cmd='bulletList' />
|
||||
<ToolButton cmd='orderedList' />
|
||||
<ToolButton cmd='img' />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -14,11 +14,12 @@ import {
|
||||
PlaceholderExtension,
|
||||
} from 'remirror/extensions';
|
||||
import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import TextEditorComponents from 'src/Components/Inputs/TextEditor';
|
||||
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar';
|
||||
import Toolbar from './Toolbar';
|
||||
import Button from 'src/Components/Button/Button';
|
||||
import { debounce } from 'remirror';
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -38,6 +39,8 @@ export default function AddComment({ initialContent, name }: Props) {
|
||||
return extension;
|
||||
}, []);
|
||||
|
||||
const valueRef = useRef<string>("");
|
||||
|
||||
|
||||
const extensions = useCallback(
|
||||
() => [
|
||||
@@ -60,33 +63,40 @@ export default function AddComment({ initialContent, name }: Props) {
|
||||
);
|
||||
|
||||
|
||||
const { manager } = useRemirror({
|
||||
const { manager, state, onChange, } = useRemirror({
|
||||
extensions,
|
||||
stringHandler: 'markdown',
|
||||
content: initialContent ?? ''
|
||||
});
|
||||
|
||||
|
||||
const submitComment = () => {
|
||||
console.log(valueRef.current);
|
||||
manager.view.updateState(manager.createState({ content: manager.createEmptyDoc() }))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className={`remirror-theme ${styles.wrapper} p-24 border rounded-12`}>
|
||||
<Remirror
|
||||
manager={manager}
|
||||
initialContent={initialContent}
|
||||
state={state}
|
||||
onChange={e => {
|
||||
const markdown = e.helpers.getMarkdown(e.state)
|
||||
valueRef.current = markdown;
|
||||
onChange(e);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-16 items-start pb-24 border-b border-gray-200 focus-within:border-primary-500">
|
||||
<div className="mt-16 shrink-0"><Avatar width={48} src='https://i.pravatar.cc/150?img=1' /></div>
|
||||
{/* <textarea
|
||||
rows={2}
|
||||
className="w-full border-0 text-gray-500 font-medium focus:!ring-0 resize-none"
|
||||
placeholder='Leave a comment...'
|
||||
ref={textAreaRef}
|
||||
/> */}
|
||||
<div className="flex-grow">
|
||||
<EditorComponent />
|
||||
<EditorComponent
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-16 mt-16">
|
||||
<Toolbar />
|
||||
<Button color='primary' className='ml-auto'>Submit</Button>
|
||||
<Button onClick={submitComment} color='primary' className='ml-auto'>Submit</Button>
|
||||
</div>
|
||||
{/* <TextEditorComponents.SaveModule name={name} /> */}
|
||||
</Remirror>
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function ContentEditor({ placeholder, initialContent, name }: Pro
|
||||
new CodeBlockExtension({
|
||||
supportedLanguages: [javascript, typescript]
|
||||
}),
|
||||
new ImageExtension({ enableResizing: true }),
|
||||
new ImageExtension(),
|
||||
// new TrailingNodeExtension(),
|
||||
// new TableExtension(),
|
||||
new MarkdownExtension({ copyAsMarkdown: false }),
|
||||
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
export default function Toolbar() {
|
||||
|
||||
return (
|
||||
<div className='flex gap-36 bg-gray-100'>
|
||||
<div className='flex flex-wrap gap-36 bg-gray-100'>
|
||||
<div className="flex">
|
||||
<TextEditorComponents.ToolButton cmd='heading' />
|
||||
<TextEditorComponents.ToolButton cmd='bold' />
|
||||
@@ -22,6 +22,7 @@ export default function Toolbar() {
|
||||
<TextEditorComponents.ToolButton cmd='rightAlign' />
|
||||
<TextEditorComponents.ToolButton cmd='bulletList' />
|
||||
<TextEditorComponents.ToolButton cmd='orderedList' />
|
||||
<TextEditorComponents.ToolButton cmd='img' />
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { WithModals } from 'src/utils/storybook/decorators';
|
||||
|
||||
import CreatePostPage from './CreatePostPage';
|
||||
|
||||
@@ -8,6 +9,9 @@ export default {
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
decorators: [
|
||||
WithModals
|
||||
]
|
||||
} as ComponentMeta<typeof CreatePostPage>;
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Login_ScanningWalletCard, Login_ExternalWalletCard, Login_NativeWalletCard, Login_SuccessCard } from "src/Components/Modals/Login";
|
||||
import { ProjectDetailsCard } from "src/features/Projects/pages/ProjectPage/ProjectDetailsCard";
|
||||
import VoteCard from "src/features/Projects/pages/ProjectPage/VoteCard/VoteCard";
|
||||
import InsertImageModal from 'src/Components/Inputs/TextEditor/InsertImageModal/InsertImageModal'
|
||||
import { Claim_FundWithdrawCard, Claim_CopySignatureCard, Claim_GenerateSignatureCard, Claim_SubmittedCard } from "src/features/Projects/pages/ProjectPage/ClaimProject";
|
||||
import { ModalCard } from "src/Components/Modals/ModalsContainer/ModalsContainer";
|
||||
import { ComponentProps } from "react";
|
||||
@@ -26,6 +27,7 @@ export const ALL_MODALS = {
|
||||
Claim_CopySignatureCard,
|
||||
Claim_SubmittedCard,
|
||||
Claim_FundWithdrawCard,
|
||||
InsertImageModal
|
||||
}
|
||||
|
||||
type ExcludeBaseModalProps<U> = Omit<U, keyof ModalCard>
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'react-loading-skeleton/dist/skeleton.css'
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { apolloClient } from '../apollo';
|
||||
import { FormProvider, useForm, UseFormProps } from 'react-hook-form';
|
||||
import ModalsContainer from 'src/Components/Modals/ModalsContainer/ModalsContainer';
|
||||
|
||||
|
||||
// Enable the Mocks Service Worker
|
||||
@@ -25,7 +26,7 @@ import { FormProvider, useForm, UseFormProps } from 'react-hook-form';
|
||||
|
||||
if (process.env.STORYBOOK_ENABLE_MOCKS) {
|
||||
worker.start({
|
||||
onUnhandledRequest: 'bypass'
|
||||
onUnhandledRequest: 'bypass',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,6 +107,7 @@ export const centerDecorator: DecoratorFn = (Story) => {
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
export const WrapForm: (options?: Partial<UseFormProps>) => DecoratorFn = options => {
|
||||
const Func: DecoratorFn = (Story) => {
|
||||
const methods = useForm(options);
|
||||
@@ -116,3 +118,8 @@ export const WrapForm: (options?: Partial<UseFormProps>) => DecoratorFn = option
|
||||
return Func
|
||||
}
|
||||
|
||||
|
||||
export const WithModals: DecoratorFn = (Component) => <>
|
||||
<Component />
|
||||
<ModalsContainer />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user