mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-04 23:14:27 +01:00
feat: BountyForm, QuestionForm, convert styling to scss
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -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 = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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 >
|
||||
)
|
||||
}
|
||||
@@ -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 = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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 >
|
||||
)
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
|
||||
@@ -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
10
src/styles/shared.scss
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user