mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-09 09:24:30 +01:00
feat: Tags Input, formProvider decorator
This commit is contained in:
23
src/Components/Inputs/TagsInput/TagsInput.stories.tsx
Normal file
23
src/Components/Inputs/TagsInput/TagsInput.stories.tsx
Normal 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({});
|
||||
75
src/Components/Inputs/TagsInput/TagsInput.tsx
Normal file
75
src/Components/Inputs/TagsInput/TagsInput.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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 <></>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user