feat: BountyForm, QuestionForm, convert styling to scss

This commit is contained in:
MTG2000
2022-04-28 13:22:18 +03:00
parent af32e190e6
commit 4661341bcc
14 changed files with 389 additions and 20 deletions

View File

@@ -12,6 +12,7 @@ interface Props {
input?: string
}
placeholder?: string
max?: number;
[k: string]: any
}
@@ -20,6 +21,7 @@ interface Props {
export default function TagsInput({
classes,
placeholder = 'Write some tags',
max = 5,
...props }: Props) {
@@ -41,13 +43,18 @@ export default function TagsInput({
onBlur();
}
const isDisabled = value.length >= max;
return (
<div className={`${classes?.container}`}>
<div className="input-wrapper relative">
<input
disabled={isDisabled}
type='text'
className={`input-text inline-block ${classes?.input}`}
className={`input-text inline-block
${isDisabled && 'opacity-50'}
${classes?.input}`}
placeholder={placeholder}
value={inputText}
onChange={(e) => setInputText(e.target.value)}

View File

@@ -0,0 +1,20 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import BountyForm from './BountyForm';
export default {
title: 'Posts/Create Post Page/Bounty Form',
component: BountyForm,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof BountyForm>;
const Template: ComponentStory<typeof BountyForm> = (args) => <div className="max-w-[1000px]"><BountyForm {...args as any} ></BountyForm></div>
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,184 @@
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 DatePicker from "src/Components/Inputs/DatePicker/DatePicker";
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
.array()
.required()
.min(1),
deadline: yup
.date()
.required(),
bounty_amount: yup
.number()
.typeError('Bounty amount must be a number')
.required()
.min(100)
.label("Bounty Amount"),
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
deadline: Date
bounty_amount: number
tags: NestedValue<object[]>
cover_image: NestedValue<File[]> | string
body: string
}
export default function BountyForm() {
const formMethods = useForm<IFormInputs>({
resolver: yupResolver(schema) as Resolver<IFormInputs>,
defaultValues: {
title: '',
tags: [],
bounty_amount: 100000,
deadline: new Date(),
body: '',
cover_image: []
}
});
const { handleSubmit, control, register, formState: { errors }, } = formMethods;
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 Bounty Title'
{...register("title")}
/>
</div>
{errors.title && <p className="input-error">
{errors.title.message}
</p>}
<div className="grid grid-cols-1 md:grid-cols-2 gap-24 mt-16">
<div>
<p className="text-body5">
Bounty Amount
</p>
<div className="input-wrapper mt-8">
<input
type="number"
className='input-text input-removed-arrows'
placeholder="10,000"
min={0}
step={100}
{...register("bounty_amount")}
/>
<p className='px-16 shrink-0 self-center text-primary-400'>
Sats
</p>
</div>
<p className='input-error'>{errors.bounty_amount?.message}</p>
</div>
<div>
<p className="text-body5">
Deadline
</p>
<Controller
name="deadline"
control={control}
render={({ field }) => <DatePicker {...field} className='mt-8' />}
/>
<p className='input-error'>{errors.deadline?.message}</p>
</div>
</div>
<p className="text-body5 mt-16">
Tags
</p>
<TagsInput
placeholder="webln, alby, lnurl, wallet, ..."
classes={{ container: 'mt-8' }}
/>
{errors.tags && <p className="input-error">
{errors.tags.message}
</p>}
</div>
<ContentEditor
placeholder="Write a detailed description for your bounty 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">
Publish
</Button>
<Button color="gray">
Save Draft
</Button>
</div>
</form>
</FormProvider >
)
}

View File

@@ -0,0 +1,20 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import QuestionForm from './QuestionForm';
export default {
title: 'Posts/Create Post Page/Question Form',
component: QuestionForm,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof QuestionForm>;
const Template: ComponentStory<typeof QuestionForm> = (args) => <div className="max-w-[1000px]"><QuestionForm {...args as any} ></QuestionForm></div>
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,125 @@
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.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) {
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: NestedValue<object[]>
cover_image: NestedValue<File[]> | string
body: string
}
export default function QuestionForm() {
const formMethods = useForm<IFormInputs>({
resolver: yupResolver(schema) as Resolver<IFormInputs>,
defaultValues: {
title: '',
tags: [],
body: '',
cover_image: []
}
});
const { handleSubmit, control, register, formState: { errors }, } = formMethods;
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 Question Title'
{...register("title")}
/>
</div>
{errors.title && <p className="input-error">
{errors.title.message}
</p>}
<p className="text-body5 mt-16">
Tags
</p>
<TagsInput
placeholder="webln, alby, lnurl, wallet, ..."
classes={{ container: 'mt-8' }}
/>
{errors.tags && <p className="input-error">
{errors.tags.message}
</p>}
</div>
<ContentEditor
placeholder="Write your question 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">
Publish
</Button>
<Button color="gray">
Save Draft
</Button>
</div>
</form>
</FormProvider >
)
}

View File

@@ -11,7 +11,7 @@ export default {
} as ComponentMeta<typeof StoryForm>;
const Template: ComponentStory<typeof StoryForm> = (args) => <StoryForm {...args as any} ></StoryForm>
const Template: ComponentStory<typeof StoryForm> = (args) => <div className="max-w-[1000px]"><StoryForm {...args as any} ></StoryForm></div>
export const Default = Template.bind({});
Default.args = {

View File

@@ -45,7 +45,7 @@ export default function StoryForm() {
title: '',
tags: [],
body: '',
cover_image: ''
cover_image: []
}
});
const { handleSubmit, control, register, formState: { errors }, } = formMethods;
@@ -113,7 +113,7 @@ export default function StoryForm() {
</div>
<div className="flex gap-16 mt-32">
<Button type='submit' color="primary">
Preview & Publish
Publish
</Button>
<Button color="gray">
Save Draft

View File

@@ -1,4 +1,6 @@
import { useState } from "react";
import BountyForm from "./Components/BountyForm/BountyForm";
import QuestionForm from "./Components/QuestionForm/QuestionForm";
import StoryForm from "./Components/StoryForm/StoryForm";
import PostTypeList from "./PostTypeList";
@@ -21,10 +23,22 @@ export default function CreatePostPage() {
<div>
{postType === 'story' && <>
<h2 className="text-h2 font-bolder text-gray-800 mb-32">
Create Story
Create a Story
</h2>
<StoryForm />
</>}
{postType === 'bounty' && <>
<h2 className="text-h2 font-bolder text-gray-800 mb-32">
Create a Bounty
</h2>
<BountyForm />
</>}
{postType === 'question' && <>
<h2 className="text-h2 font-bolder text-gray-800 mb-32">
Create a Question
</h2>
<QuestionForm />
</>}
</div>
</div>
)

View File

@@ -4,10 +4,8 @@ import { AiFillThunderbolt } from 'react-icons/ai'
import { IoClose } from 'react-icons/io5'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer';
import { useAppSelector } from 'src/utils/hooks';
import { gql, useMutation, useApolloClient } from "@apollo/client";
import Confetti from "react-confetti";
import { Wallet_Service } from 'src/services';
import styles from './style.module.css'
import { useWindowSize } from '@react-hookz/web';
import { useConfirmVoteMutation, useVoteMutation } from 'src/graphql';
@@ -121,7 +119,7 @@ export default function VoteCard({ onClose, direction, projectId, initVotes, ...
</label>
<div className="input-wrapper">
<input
className={` input-text ${styles.input} `}
className={` input-text input-removed-arrows`}
value={voteAmount} onChange={onChangeInput}
type="number"
placeholder="e.g 5 sats" />

View File

@@ -1,10 +0,0 @@
.input::-webkit-outer-spin-button,
.input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.input[type="number"] {
-moz-appearance: textfield;
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import Wrapper from './utils/Wrapper';
import './index.scss';
import './styles/index.scss';
import App from './App';

View File

@@ -1,4 +1,5 @@
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap");
@import url("./shared.scss");
@tailwind base;
@tailwind components;
@tailwind utilities;

10
src/styles/shared.scss Normal file
View File

@@ -0,0 +1,10 @@
.input-removed-arrows::-webkit-outer-spin-button,
.input-removed-arrows::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.input-removed-arrows[type="number"] {
-moz-appearance: textfield;
}

View File

@@ -11,7 +11,7 @@ import { AnimatePresence, motion } from 'framer-motion';
// Add the global stuff first (index.ts)
// -------------------------------------------
import "src/index.scss";
import "src/styles/index.scss";
import "react-multi-carousel/lib/styles.css";
import 'react-loading-skeleton/dist/skeleton.css'
import { ApolloProvider } from '@apollo/client';