feat: Tags Input, formProvider decorator

This commit is contained in:
MTG2000
2022-04-27 15:29:18 +03:00
parent af253c980e
commit cad9acdfac
7 changed files with 138 additions and 21 deletions

View File

@@ -0,0 +1,23 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { WrapForm } from 'src/utils/storybook/decorators';
import TagsInput from './TagsInput';
export default {
title: 'Shared/TagsInput',
component: TagsInput,
argTypes: {
backgroundColor: { control: 'color' },
},
decorators: [WrapForm()]
} as ComponentMeta<typeof TagsInput>;
const Template: ComponentStory<typeof TagsInput> = (args) => <div>
<p className="text-body4 mb-8 text-gray-700">
Enter Tags:
</p>
<TagsInput classes={{ input: "max-w-[320px]" }} {...args}></TagsInput>
</div>
export const Default = Template.bind({});

View File

@@ -0,0 +1,75 @@
import { motion } from "framer-motion";
import { useState } from "react";
import { useController } from "react-hook-form";
import Badge from "src/Components/Badge/Badge";
import { Tag as ApiTag } from "src/utils/interfaces";
type Tag = Pick<ApiTag, 'title'>
interface Props {
classes?: {
container?: string
input?: string
}
placeholder?: string
[k: string]: any
}
export default function TagsInput({
classes,
placeholder = 'Write some tags',
...props }: Props) {
const [inputText, setInputText] = useState("");
const { field: { value, onChange, onBlur } } = useController({
name: props.name ?? "tags",
control: props.control,
})
const handleSubmit = () => {
onChange([...value, { title: inputText }]);
setInputText('');
onBlur();
}
const handleRemove = (idx: number) => {
onChange((value as Tag[]).filter((_, i) => idx !== i))
onBlur();
}
return (
<div className={`${classes?.container}`}>
<div className="input-wrapper relative">
<input
type='text'
className={`input-text inline-block ${classes?.input}`}
placeholder={placeholder}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter' && inputText.trim().length > 1) { e.preventDefault(); handleSubmit() }
}}
/>
{inputText.length > 2 && <motion.span
initial={{ scale: 1, y: "-50%" }}
animate={{ scale: 1.05 }}
transition={{
repeat: Infinity,
repeatType: 'mirror',
duration: .9
}}
className="text-gray-500 absolute top-1/2 right-16">
Enter to Insert
</motion.span>}
</div>
<div className="flex mt-16 gap-8 flex-wrap">
{(value as Tag[]).map((tag, idx) => <Badge color="gray" key={tag.title} onRemove={() => handleRemove(idx)} >{tag.title}</Badge>)}
</div>
</div>
)
}

View File

@@ -1,11 +1,12 @@
import React from 'react'
import React, { useCallback } from 'react'
import { EditorComponent, Remirror, useHelpers, useRemirror, useEvent, useEditorState } from '@remirror/react';
import { Control, useController } from 'react-hook-form';
interface Props {
control?: Control,
name?: string
name?: string,
}
export default function SaveModule(props: Props) {
@@ -17,10 +18,12 @@ export default function SaveModule(props: Props) {
name: props.name ?? 'content'
})
useEvent('blur', () => {
const listener = (d: any) => {
onChange(getMarkdown(state));
onBlur()
})
};
useEvent('blur', listener)
return <></>
}

View File

@@ -81,10 +81,12 @@ export default function TextEditor({ placeholder, initialContent }: Props) {
[linkExtension, placeholder],
);
const { manager, } = useRemirror({
const { manager } = useRemirror({
extensions,
stringHandler: 'markdown',
});
return (
<div className={`remirror-theme ${styles.wrapper} bg-white shadow-md`}>
<Remirror

View File

@@ -26,7 +26,7 @@ import {
} from 'remirror/extensions';
import { ExtensionPriority } from 'remirror';
import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import TextEditorComponents from 'src/Components/Inputs/TextEditor';
import Toolbar from './Toolbar';
@@ -82,10 +82,13 @@ export default function ContentEditor({ placeholder, initialContent, name }: Pro
[linkExtension, placeholder],
);
const { manager, } = useRemirror({
const { manager } = useRemirror({
extensions,
stringHandler: 'markdown',
});
return (
<div className={`remirror-theme ${styles.wrapper} bg-white`}>
<Remirror

View File

@@ -2,13 +2,14 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form";
import Button from "src/Components/Button/Button";
import FilesInput from "src/Components/Inputs/FilesInput/FilesInput";
import TagsInput from "src/Components/Inputs/TagsInput/TagsInput";
import * as yup from "yup";
import ContentEditor from "../ContentEditor/ContentEditor";
const schema = yup.object({
title: yup.string().required().min(10),
tags: yup.string().required().min(10),
tags: yup.array().required().min(1),
body: yup.string().required().min(50, 'you have to write at least 10 words'),
cover_image: yup.lazy((value: string | File[]) => {
switch (typeof value) {
@@ -28,7 +29,7 @@ const schema = yup.object({
interface IFormInputs {
title: string
tags: string
tags: NestedValue<object[]>
cover_image: NestedValue<File[]> | string
body: string
}
@@ -41,14 +42,16 @@ export default function StoryForm() {
const formMethods = useForm<IFormInputs>({
resolver: yupResolver(schema) as Resolver<IFormInputs>,
defaultValues: {
title: '',
tags: [{
title: 'tag 1'
}],
body: '',
cover_image: 'https://i.picsum.photos/id/10/1600/900.jpg?hmac=9R7fIkKwC5JxHx8ayZAKNMt6FvJXqKKyiv8MClikgDo'
}
});
const { handleSubmit, control, register, formState: { isValid, errors }, watch, } = formMethods;
console.log(errors);
const { handleSubmit, control, register, formState: { errors }, watch, } = formMethods;
const onSubmit: SubmitHandler<IFormInputs> = data => console.log(data);
@@ -96,14 +99,10 @@ export default function StoryForm() {
<p className="text-body5 mt-16">
Tags
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='WebLN, Design, ...'
{...register("tags")}
/>
</div>
<TagsInput
placeholder="webln, alby, lnurl, wallet, ..."
classes={{ container: 'mt-8' }}
/>
{errors.tags && <p className="input-error">
{errors.tags.message}
</p>}

View File

@@ -16,6 +16,7 @@ import "react-multi-carousel/lib/styles.css";
import 'react-loading-skeleton/dist/skeleton.css'
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from '../apollo';
import { FormProvider, useForm, UseFormProps } from 'react-hook-form';
// Enable the Mocks Service Worker
@@ -96,4 +97,15 @@ export const centerDecorator: DecoratorFn = (Story) => {
return <div className="min-h-screen flex justify-center items-center">
<Story />
</div>
}
}
export const WrapForm: (options?: Partial<UseFormProps>) => DecoratorFn = options => {
const Func: DecoratorFn = (Story) => {
const methods = useForm(options);
return <FormProvider {...methods} >
<Story />
</FormProvider>
}
return Func
}