mirror of
https://github.com/aljazceru/landscape-template.git
synced 2025-12-17 06:14:27 +01:00
feat: Story form, Post content editor, File Input Component, File thumbnail component, change from css to scss
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import "../src/index.css";
|
||||
import { configure, addDecorator, addParameters } from "@storybook/react";
|
||||
import { WrapperDecorator, AppDecorator } from 'src/utils/storybook/decorators'
|
||||
|
||||
|
||||
24
src/Components/Inputs/FilesInput/FileInput.stories.tsx
Normal file
24
src/Components/Inputs/FilesInput/FileInput.stories.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { BsImages } from 'react-icons/bs';
|
||||
import Button from 'src/Components/Button/Button';
|
||||
|
||||
import FilesInput from './FilesInput';
|
||||
|
||||
export default {
|
||||
title: 'Shared/Files Input',
|
||||
component: FilesInput,
|
||||
|
||||
} as ComponentMeta<typeof FilesInput>;
|
||||
|
||||
const Template: ComponentStory<typeof FilesInput> = (args) => <FilesInput {...args} />
|
||||
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
export const CustomButton = Template.bind({});
|
||||
CustomButton.args = {
|
||||
multiple: true,
|
||||
uploadBtn: <Button color='primary'><span className="align-middle">Drop Images</span> <BsImages className='ml-12 scale-125' /></Button>
|
||||
}
|
||||
75
src/Components/Inputs/FilesInput/FileThumbnail.tsx
Normal file
75
src/Components/Inputs/FilesInput/FileThumbnail.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useMemo } from "react";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import IconButton from "src/Components/IconButton/IconButton";
|
||||
|
||||
interface Props {
|
||||
file: File | string,
|
||||
onRemove?: () => void
|
||||
}
|
||||
|
||||
function getFileType(file: File | string) {
|
||||
if (typeof file === 'string') {
|
||||
if (/^http[^?]*.(jpg|jpeg|gif|png|tiff|bmp)(\?(.*))?$/gmi.test(file))
|
||||
return 'image'
|
||||
if (/\.(pdf|doc|docx)$/.test(file))
|
||||
return 'document';
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
else {
|
||||
if (file['type'].split('/')[0] === 'image')
|
||||
return 'image'
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
}
|
||||
|
||||
type ThumbnailFile = {
|
||||
name: string;
|
||||
src: string;
|
||||
type: ReturnType<typeof getFileType>
|
||||
}
|
||||
|
||||
function processFile(file: Props['file']): ThumbnailFile {
|
||||
|
||||
const fileType = getFileType(file);
|
||||
|
||||
if (typeof file === 'string') return { name: file, src: file, type: fileType };
|
||||
|
||||
return {
|
||||
name: file.name,
|
||||
src: URL.createObjectURL(file),
|
||||
type: fileType
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default function FileThumbnail({ file: f, onRemove }: Props) {
|
||||
|
||||
const file = useMemo(() => processFile(f), [f])
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 rounded-8 p-12 shrink-0 flex gap-4 overflow-hidden">
|
||||
<div className="w-[100px]">
|
||||
<p className="text-body6 overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
{file.name}
|
||||
</p>
|
||||
<a
|
||||
href={file.src}
|
||||
target='_blank'
|
||||
rel="noreferrer"
|
||||
>
|
||||
{
|
||||
file.type === 'image' && <img src={file.src} alt={file.name} className="p-4 w-3/4 mx-auto max-h-full object-contain" />
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
<div className="w-32 shrink-0 self-start" >
|
||||
<IconButton size="sm" className="hover:bg-gray-500" onClick={onRemove}>
|
||||
<MdClose />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
104
src/Components/Inputs/FilesInput/FilesInput.tsx
Normal file
104
src/Components/Inputs/FilesInput/FilesInput.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { ChangeEvent, useRef } from "react"
|
||||
import { BsUpload } from "react-icons/bs";
|
||||
import Button from "src/Components/Button/Button"
|
||||
import { UnionToObjectKeys } from "src/utils/types/utils";
|
||||
import FilesThumbnails from "./FilesThumbnails";
|
||||
|
||||
|
||||
type Props = {
|
||||
multiple?: boolean;
|
||||
value?: File[] | string[] | string;
|
||||
max?: number;
|
||||
onBlur?: () => void;
|
||||
onChange?: (files: (File | string)[] | null) => void
|
||||
uploadBtn?: JSX.Element
|
||||
uploadText?: string;
|
||||
allowedType?: 'images';
|
||||
}
|
||||
|
||||
const fileAccept: UnionToObjectKeys<Props, 'allowedType'> = {
|
||||
images: ".png, .jpg, .jpeg"
|
||||
} as const;
|
||||
|
||||
const fileUrlToObject = async (url: string, fileName: string = 'filename') => {
|
||||
const res = await fetch(url);
|
||||
const contentType = res.headers.get('content-type') as string;
|
||||
const blob = await res.blob()
|
||||
const file = new File([blob], fileName, { contentType } as any)
|
||||
return file
|
||||
}
|
||||
|
||||
export default function FilesInput({
|
||||
multiple,
|
||||
value,
|
||||
max = 3,
|
||||
onBlur,
|
||||
onChange,
|
||||
allowedType = 'images',
|
||||
uploadText = 'Upload files',
|
||||
...props
|
||||
}: Props) {
|
||||
|
||||
const ref = useRef<HTMLInputElement>(null!)
|
||||
|
||||
const handleClick = () => {
|
||||
ref.current.click();
|
||||
}
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files && Array.from(e.target.files).slice(0, max);
|
||||
if (typeof value === 'string')
|
||||
onChange?.([value, ...(files ?? [])]);
|
||||
else
|
||||
onChange?.([...(value ?? []), ...(files ?? [])]);
|
||||
}
|
||||
|
||||
const handleRemove = async (idx: number) => {
|
||||
if (!value) return onChange?.([]);
|
||||
if (typeof value === 'string')
|
||||
onChange?.([]);
|
||||
else {
|
||||
let files = [...value]
|
||||
files.splice(idx, 1);
|
||||
|
||||
//change all files urls to file objects
|
||||
const filesConverted = await Promise.all(files.map(async file => {
|
||||
if (typeof file === 'string') return await fileUrlToObject(file, "")
|
||||
else return file;
|
||||
}))
|
||||
|
||||
onChange?.(filesConverted);
|
||||
}
|
||||
}
|
||||
|
||||
const canUploadMore = multiple ?
|
||||
!value || (value && value.length < max)
|
||||
:
|
||||
!value || value.length === 0
|
||||
|
||||
|
||||
const uploadBtn = props.uploadBtn ?
|
||||
React.cloneElement(props.uploadBtn, { onClick: handleClick })
|
||||
:
|
||||
<Button type='button' onClick={handleClick} ><span className="align-middle">{uploadText}</span> <BsUpload className="ml-12 scale-125" /></Button>
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilesThumbnails files={value} onRemove={handleRemove} />
|
||||
{
|
||||
canUploadMore &&
|
||||
<>
|
||||
{uploadBtn}
|
||||
<input
|
||||
ref={ref}
|
||||
type="file"
|
||||
onBlur={onBlur}
|
||||
style={{ display: 'none' }}
|
||||
multiple={multiple}
|
||||
accept={fileAccept[allowedType]}
|
||||
onChange={handleChange} />
|
||||
</>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
31
src/Components/Inputs/FilesInput/FilesThumbnails.tsx
Normal file
31
src/Components/Inputs/FilesInput/FilesThumbnails.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { MdClose } from 'react-icons/md';
|
||||
import IconButton from 'src/Components/IconButton/IconButton';
|
||||
import FileThumbnail from './FileThumbnail';
|
||||
|
||||
interface Props {
|
||||
files?: (File | string)[] | string;
|
||||
onRemove?: (idx: number) => void
|
||||
}
|
||||
|
||||
function processFiles(files: Props['files']) {
|
||||
|
||||
if (!files) return [];
|
||||
if (typeof files === 'string') return [files];
|
||||
return files;
|
||||
}
|
||||
|
||||
export default function FilesThumbnails({ files, onRemove }: Props) {
|
||||
const filesConverted = useMemo(() => processFiles(files), [files])
|
||||
|
||||
return (
|
||||
<div className="flex gap-12 mb-12">
|
||||
{
|
||||
filesConverted.map((file, idx) => <FileThumbnail
|
||||
key={idx}
|
||||
file={file}
|
||||
onRemove={() => onRemove?.(idx)} />)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
26
src/Components/Inputs/TextEditor/SaveModule.tsx
Normal file
26
src/Components/Inputs/TextEditor/SaveModule.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React 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
|
||||
}
|
||||
|
||||
export default function SaveModule(props: Props) {
|
||||
|
||||
const state = useEditorState()
|
||||
const { getMarkdown } = useHelpers();
|
||||
const { field: { onChange, onBlur } } = useController({
|
||||
control: props.control,
|
||||
name: props.name ?? 'content'
|
||||
})
|
||||
|
||||
useEvent('blur', () => {
|
||||
onChange(getMarkdown(state));
|
||||
onBlur()
|
||||
})
|
||||
|
||||
return <></>
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import TextEditor from './TextEditor';
|
||||
|
||||
@@ -8,8 +9,16 @@ export default {
|
||||
|
||||
} as ComponentMeta<typeof TextEditor>;
|
||||
|
||||
const Template: ComponentStory<typeof TextEditor> = (args) => <TextEditor {...args as any} />
|
||||
const Template: ComponentStory<typeof TextEditor> = (args) => {
|
||||
|
||||
const methods = useForm();
|
||||
|
||||
console.log(methods.watch('content'))
|
||||
|
||||
return <FormProvider {...methods}>
|
||||
<TextEditor {...args} />
|
||||
</FormProvider>
|
||||
}
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
|
||||
@@ -25,9 +25,10 @@ import {
|
||||
UnderlineExtension,
|
||||
} from 'remirror/extensions';
|
||||
import { ExtensionPriority } from 'remirror';
|
||||
import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
|
||||
import { EditorComponent, Remirror, useHelpers, useRemirror } from '@remirror/react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import Toolbar from './Toolbar/Toolbar';
|
||||
import SaveModule from './SaveModule';
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -46,6 +47,7 @@ export default function TextEditor({ placeholder, initialContent }: Props) {
|
||||
return extension;
|
||||
}, []);
|
||||
|
||||
|
||||
const extensions = useCallback(
|
||||
() => [
|
||||
new PlaceholderExtension({ placeholder }),
|
||||
@@ -76,7 +78,7 @@ export default function TextEditor({ placeholder, initialContent }: Props) {
|
||||
*/
|
||||
new HardBreakExtension(),
|
||||
],
|
||||
[placeholder],
|
||||
[linkExtension, placeholder],
|
||||
);
|
||||
|
||||
const { manager, } = useRemirror({
|
||||
@@ -85,7 +87,12 @@ export default function TextEditor({ placeholder, initialContent }: Props) {
|
||||
});
|
||||
return (
|
||||
<div className={`remirror-theme ${styles.wrapper} bg-white shadow-md`}>
|
||||
<Remirror manager={manager} initialContent={initialContent}>
|
||||
<Remirror
|
||||
manager={manager}
|
||||
initialContent={initialContent}
|
||||
|
||||
>
|
||||
<SaveModule />
|
||||
<Toolbar />
|
||||
<EditorComponent />
|
||||
</Remirror>
|
||||
|
||||
@@ -34,24 +34,21 @@ export default function ToolButton({ cmd: _cmd }: Props) {
|
||||
|
||||
if (_cmd === 'heading') {
|
||||
return <Menu menuButton={
|
||||
<MenuButton>
|
||||
<button
|
||||
className={`
|
||||
<MenuButton className={`
|
||||
w-36 h-36 flex justify-center items-center
|
||||
${active.heading({}) ?
|
||||
'font-bold bg-gray-300 text-black'
|
||||
:
|
||||
'hover:bg-gray-200'
|
||||
}
|
||||
'font-bold bg-gray-300 text-black'
|
||||
:
|
||||
'hover:bg-gray-200'
|
||||
}
|
||||
${!commands.toggleHeading.enabled() && 'opacity-40 text-gray-600 pointer-events-none'}
|
||||
|
||||
`}
|
||||
>
|
||||
<FiType />
|
||||
</button>
|
||||
`}>
|
||||
<FiType />
|
||||
</MenuButton>
|
||||
} transition>
|
||||
{Array(6).fill(0).map((_, idx) => <MenuItem
|
||||
key={idx}
|
||||
className={`
|
||||
py-8 px-16 hover:bg-gray-200
|
||||
${active.heading({ level: idx + 1 }) && 'font-bold bg-gray-200'}
|
||||
|
||||
5
src/Components/Inputs/TextEditor/index.tsx
Normal file
5
src/Components/Inputs/TextEditor/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import SaveModule from "./SaveModule";
|
||||
import ToolButton from "./Toolbar/ToolButton";
|
||||
|
||||
const TextEditorComponents = { SaveModule, ToolButton };
|
||||
export default TextEditorComponents;
|
||||
@@ -0,0 +1,103 @@
|
||||
import 'remirror/styles/all.css';
|
||||
import styles from './styles.module.scss'
|
||||
|
||||
import javascript from 'refractor/lang/javascript';
|
||||
import typescript from 'refractor/lang/typescript';
|
||||
import {
|
||||
BlockquoteExtension,
|
||||
BoldExtension,
|
||||
BulletListExtension,
|
||||
CodeBlockExtension,
|
||||
CodeExtension,
|
||||
HardBreakExtension,
|
||||
HeadingExtension,
|
||||
ImageExtension,
|
||||
ItalicExtension,
|
||||
LinkExtension,
|
||||
ListItemExtension,
|
||||
MarkdownExtension,
|
||||
NodeFormattingExtension,
|
||||
OrderedListExtension,
|
||||
PlaceholderExtension,
|
||||
StrikeExtension,
|
||||
TableExtension,
|
||||
TrailingNodeExtension,
|
||||
UnderlineExtension,
|
||||
} from 'remirror/extensions';
|
||||
import { ExtensionPriority } from 'remirror';
|
||||
import { EditorComponent, Remirror, useRemirror } from '@remirror/react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import TextEditorComponents from 'src/Components/Inputs/TextEditor';
|
||||
import Toolbar from './Toolbar';
|
||||
|
||||
|
||||
interface Props {
|
||||
placeholder?: string;
|
||||
initialContent?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export default function ContentEditor({ placeholder, initialContent, name }: Props) {
|
||||
|
||||
const linkExtension = useMemo(() => {
|
||||
const extension = new LinkExtension({ autoLink: true });
|
||||
extension.addHandler('onClick', (_, data) => {
|
||||
alert(`You clicked link: ${JSON.stringify(data)}`);
|
||||
return true;
|
||||
});
|
||||
return extension;
|
||||
}, []);
|
||||
|
||||
|
||||
const extensions = useCallback(
|
||||
() => [
|
||||
new PlaceholderExtension({ placeholder }),
|
||||
linkExtension,
|
||||
new BoldExtension(),
|
||||
// new StrikeExtension(),
|
||||
new UnderlineExtension(),
|
||||
new ItalicExtension(),
|
||||
new HeadingExtension(),
|
||||
new LinkExtension(),
|
||||
new BlockquoteExtension(),
|
||||
new BulletListExtension(),
|
||||
new OrderedListExtension(),
|
||||
new ListItemExtension({ priority: ExtensionPriority.High, enableCollapsible: true }),
|
||||
// new TaskListExtension(),
|
||||
new CodeExtension(),
|
||||
new CodeBlockExtension({
|
||||
supportedLanguages: [javascript, typescript]
|
||||
}),
|
||||
new ImageExtension({ enableResizing: true }),
|
||||
// new TrailingNodeExtension(),
|
||||
// new TableExtension(),
|
||||
new MarkdownExtension({ copyAsMarkdown: false }),
|
||||
new NodeFormattingExtension(),
|
||||
/**
|
||||
* `HardBreakExtension` allows us to create a newline inside paragraphs.
|
||||
* e.g. in a list item
|
||||
*/
|
||||
new HardBreakExtension(),
|
||||
],
|
||||
[linkExtension, placeholder],
|
||||
);
|
||||
|
||||
const { manager, } = useRemirror({
|
||||
extensions,
|
||||
stringHandler: 'markdown',
|
||||
});
|
||||
return (
|
||||
<div className={`remirror-theme ${styles.wrapper} bg-white`}>
|
||||
<Remirror
|
||||
manager={manager}
|
||||
initialContent={initialContent}
|
||||
>
|
||||
<TextEditorComponents.SaveModule name={name} />
|
||||
<Toolbar />
|
||||
<EditorComponent />
|
||||
</Remirror>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
import TextEditorComponents from 'src/Components/Inputs/TextEditor';
|
||||
|
||||
interface Props {
|
||||
}
|
||||
|
||||
export default function Toolbar() {
|
||||
|
||||
return (
|
||||
<div className='flex gap-36 bg-gray-100'>
|
||||
<div className="flex">
|
||||
<TextEditorComponents.ToolButton cmd='heading' />
|
||||
<TextEditorComponents.ToolButton cmd='bold' />
|
||||
<TextEditorComponents.ToolButton cmd='italic' />
|
||||
<TextEditorComponents.ToolButton cmd='underline' />
|
||||
<TextEditorComponents.ToolButton cmd='code' />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<TextEditorComponents.ToolButton cmd='leftAlign' />
|
||||
<TextEditorComponents.ToolButton cmd='centerAlign' />
|
||||
<TextEditorComponents.ToolButton cmd='rightAlign' />
|
||||
<TextEditorComponents.ToolButton cmd='bulletList' />
|
||||
<TextEditorComponents.ToolButton cmd='orderedList' />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex ml-auto">
|
||||
<TextEditorComponents.ToolButton cmd='undo' />
|
||||
<TextEditorComponents.ToolButton cmd='redo' />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
.wrapper {
|
||||
|
||||
:global{
|
||||
|
||||
.ProseMirror {
|
||||
overflow: hidden;
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
|
||||
.ProseMirror,
|
||||
.ProseMirror:active,
|
||||
.ProseMirror:focus{
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
|
||||
import StoryForm from './StoryForm';
|
||||
|
||||
export default {
|
||||
title: 'Posts/Create Post Page/Story Form',
|
||||
component: StoryForm,
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
} as ComponentMeta<typeof StoryForm>;
|
||||
|
||||
|
||||
const Template: ComponentStory<typeof StoryForm> = (args) => <StoryForm {...args as any} ></StoryForm>
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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 * 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),
|
||||
body: yup.string().required().min(50, 'you have to write at least 10 words'),
|
||||
cover_image: yup.lazy((value: string | File[]) => {
|
||||
switch (typeof value) {
|
||||
case 'object':
|
||||
return yup.array()
|
||||
.test("fileSize", "File Size is too large", (files) => (files as File[]).every(file => file.size <= 5242880))
|
||||
.test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed",
|
||||
(files) => (files as File[]).every((file: File) =>
|
||||
["image/jpeg", "image/png", "image/jpg"].includes(file.type)))
|
||||
case 'string':
|
||||
return yup.string().url();
|
||||
default:
|
||||
return yup.mixed()
|
||||
}
|
||||
})
|
||||
}).required();
|
||||
|
||||
interface IFormInputs {
|
||||
title: string
|
||||
tags: string
|
||||
cover_image: NestedValue<File[]> | string
|
||||
body: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function StoryForm() {
|
||||
|
||||
|
||||
const formMethods = useForm<IFormInputs>({
|
||||
resolver: yupResolver(schema) as Resolver<IFormInputs>,
|
||||
defaultValues: {
|
||||
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 onSubmit: SubmitHandler<IFormInputs> = data => console.log(data);
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<div
|
||||
className='bg-white shadow-lg rounded-8 overflow-hidden'>
|
||||
<div className="p-32">
|
||||
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="cover_image"
|
||||
render={({ field: { onChange, value, onBlur } }) => (
|
||||
<FilesInput
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
uploadText='Add a cover image'
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className='input-error'>{errors.cover_image?.message}</p>
|
||||
|
||||
|
||||
<p className="text-body5 mt-16">
|
||||
Title
|
||||
</p>
|
||||
<div className="input-wrapper mt-8 relative">
|
||||
<input
|
||||
type='text'
|
||||
className="input-text"
|
||||
placeholder='Your Story Title'
|
||||
{...register("title")}
|
||||
/>
|
||||
</div>
|
||||
{errors.title && <p className="input-error">
|
||||
{errors.title.message}
|
||||
</p>}
|
||||
|
||||
|
||||
<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>
|
||||
{errors.tags && <p className="input-error">
|
||||
{errors.tags.message}
|
||||
</p>}
|
||||
</div>
|
||||
<ContentEditor
|
||||
placeholder="Write your story content here..."
|
||||
name="body"
|
||||
/>
|
||||
|
||||
{errors.body && <p className="input-error py-8 px-16">
|
||||
{errors.body.message}
|
||||
</p>}
|
||||
</div>
|
||||
<div className="flex gap-16 mt-32">
|
||||
<Button type='submit' color="primary">
|
||||
Preview & Publish
|
||||
</Button>
|
||||
<Button color="gray">
|
||||
Save Draft
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider >
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
export default function CreatePostPage() {
|
||||
return (
|
||||
<div>CreatePostPage</div>
|
||||
)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export default function Login_SuccessCard({ onClose, direction, ...props }: Moda
|
||||
rows={6}
|
||||
className="input-text !p-20"
|
||||
placeholder='What steps will you take to complete this task? '
|
||||
{...register("workplan", { required: true, minLength: 20 })}
|
||||
{...register("workplan")}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 gap-24 mt-16">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.input-text {
|
||||
@apply flex-grow border-none focus:border-0 focus:!ring-0 bg-transparent min-w-0 text-primary-500;
|
||||
@apply flex-grow border-none focus:border-0 focus:ring-0 bg-transparent min-w-0 text-primary-500;
|
||||
}
|
||||
|
||||
.input-checkbox {
|
||||
@@ -34,6 +34,10 @@
|
||||
@apply h-full text-primary-500 flex-shrink-0 w-42 px-12 self-center;
|
||||
}
|
||||
|
||||
.input-error{
|
||||
@apply text-body6 text-red-500 mt-4;
|
||||
}
|
||||
|
||||
.chip {
|
||||
@apply bg-gray-100 text-body4 px-16 py-8 rounded-24 font-regular;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import Wrapper from './utils/Wrapper';
|
||||
import './index.css';
|
||||
import './index.scss';
|
||||
import App from './App';
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
// Add the global stuff first (index.ts)
|
||||
// -------------------------------------------
|
||||
|
||||
import "src/index.scss";
|
||||
import "react-multi-carousel/lib/styles.css";
|
||||
import 'react-loading-skeleton/dist/skeleton.css'
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
|
||||
Reference in New Issue
Block a user