diff --git a/core/ui/package.json b/core/ui/package.json index 9195fd34..1aa83cef 100644 --- a/core/ui/package.json +++ b/core/ui/package.json @@ -58,6 +58,7 @@ "dayjs": "^1.11.1", "qs": "^6.10.3", "rc-notification": "^4.5.7", + "react-hook-form": "^7.43.1", "react-infinite-scroll-component": "^6.1.0", "web3modal": "^1.9.9" }, diff --git a/core/ui/src/components/Auction/AuctionForm/index.tsx b/core/ui/src/components/Auction/AuctionForm/index.tsx deleted file mode 100644 index aa9dbf1a..00000000 --- a/core/ui/src/components/Auction/AuctionForm/index.tsx +++ /dev/null @@ -1,481 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import { Checkbox } from 'components/Checkbox'; -import { AuctionNftHeader } from '../AuctionNftHeader'; - -import { SingleTokenInfo } from '@liqnft/candy-shop-sdk'; -import dayjs from 'dayjs'; - -import './style.less'; -import { EMPTY_FUNCTION } from 'utils/helperFunc'; -import { convertTime12to24 } from 'utils/timer'; - -interface AuctionFormProps { - onSubmit: (...args: any) => void; - currencySymbol?: string; - fee?: number; - nft: SingleTokenInfo; - auctionForm?: FormType; - onBack: () => void; - showExtensionBidding: boolean; -} - -enum CheckEnum { - PERIOD = 'biddingPeriod', - CLOCK_FORMAT = 'clockFormat', - CLOCK_FORMAT_END = 'clockFormatEnd', - BUY_NOW = 'buyNow', - START_NOW = 'startNow', - DISABLE_BIDDING_EXTENSION = 'disableBiddingExtension', - EXTENSION_PERIOD = 'extensionPeriod' -} - -export type FormType = { - startingBid: string; - buyNowPrice: string; - biddingPeriod: number; - clockFormat: 'PM' | 'AM'; - auctionHour: string; - auctionMinute: string; - clockFormatEnd: 'PM' | 'AM'; - auctionHourEnd: string; - auctionMinuteEnd: string; - buyNow?: boolean; - startNow?: boolean; - startDate: string; - endDate: string; - tickSize: string; - disableBiddingExtension: boolean; - extensionPeriod: string; -}; - -const VALIDATE_MESSAGE: { [key: string]: string } = { - startingBid: 'Starting Bid must be greater than 0.', - tickSize: 'Minimum Incremental Bid must be greater than 0.', - buyNowPrice: 'Buy Now Price must be greater than 0.', - extensionPeriod: '' -}; - -const validateInput = (nodeId: string, message: string) => { - (document.getElementById(nodeId) as HTMLInputElement)?.setCustomValidity(message); -}; - -const reportValidity = () => { - (document.getElementById('auction-form') as HTMLFormElement).reportValidity(); -}; - -const onResetValidation = () => { - Object.keys(VALIDATE_MESSAGE) - .concat('startDate', 'endDate') - .forEach((nodeId) => (document.getElementById(nodeId) as HTMLInputElement)?.setCustomValidity('')); -}; - -export const AuctionForm: React.FC = ({ - onSubmit, - currencySymbol, - fee, - nft, - auctionForm, - onBack, - showExtensionBidding -}) => { - const [form, setForm] = useState({ - startingBid: '', - tickSize: '', - buyNowPrice: '', - biddingPeriod: 24, - clockFormat: 'AM', - auctionHour: '12', - auctionMinute: '00', - clockFormatEnd: 'AM', - auctionHourEnd: '12', - auctionMinuteEnd: '00', - startNow: false, - buyNow: false, - startDate: dayjs().add(1, 'd').format('YYYY-MM-DD'), - endDate: dayjs().add(3, 'd').format('YYYY-MM-DD'), - disableBiddingExtension: false, - extensionPeriod: '' - }); - - const onCheck = (key: CheckEnum, value?: any) => (e: any) => { - e.preventDefault(); - onResetValidation(); - setForm((prev: FormType) => ({ ...prev, [key]: value })); - }; - - const onCheckbox = (key: CheckEnum) => (e: any) => { - e.preventDefault(); - onResetValidation(); - setForm((prev: FormType) => ({ ...prev, [key]: !prev[key] })); - }; - - const onChangeInput = (e: React.ChangeEvent) => { - onResetValidation(); - const { value, name } = e.target as { value: any; name: keyof FormType }; - - if (name !== 'startDate' && name !== 'endDate') { - validateInput(name, Number(value) > 0 ? '' : VALIDATE_MESSAGE[name]); - } - if (name === 'buyNowPrice' && form.buyNow) { - const minBuyNowPrice = Number(form.startingBid) + Number(form.tickSize); - - validateInput( - name, - Number(value) > minBuyNowPrice ? '' : `Buy Now Price must be greater than ${minBuyNowPrice}.` - ); - } - setForm((prev: FormType) => ({ ...prev, [name]: value })); - }; - - const onSubmitForm = (e: any) => { - e.preventDefault(); - - const VALIDATES: { nodeId: keyof FormType; trigger: boolean }[] = [ - { nodeId: 'startingBid', trigger: true }, - { nodeId: 'tickSize', trigger: true }, - { nodeId: 'buyNowPrice', trigger: Boolean(form.buyNow) } - ]; - - if ( - VALIDATES.some(({ nodeId, trigger }) => { - if (!trigger) return false; - if (form[nodeId] === '') { - return true; - } - return Number(form[nodeId]) <= 0; - }) - ) { - return; - } - - const NOW = dayjs().unix(); - - const startDate = form.startNow - ? NOW - : dayjs(`${form.startDate} ${convertTime12to24(form.auctionHour, form.auctionMinute, form.clockFormat)}`).unix(); - - const endDate = dayjs( - `${form.endDate} ${convertTime12to24(form.auctionHourEnd, form.auctionMinuteEnd, form.clockFormatEnd)}` - ).unix(); - - const biddingPeriod = (endDate - startDate) / (60 * 60); - - if (biddingPeriod <= 0) { - validateInput('endDate', `End time must be > Start time`); - return reportValidity(); - } - - if (startDate < NOW) { - validateInput('startDate', `Start time must be > current time.`); - return reportValidity(); - } - - onSubmit({ ...form, biddingPeriod }); - }; - - const preventUpdateNumberOnWheel = (e: any) => { - e.preventDefault(); - e.currentTarget.blur(); - }; - - useEffect(() => { - if (auctionForm) setForm(auctionForm); - }, [auctionForm]); - - return ( -
- - -
- - - {currencySymbol} -
- -
- - - {currencySymbol} -
- -
-
Fees
-
{fee ? `${fee.toFixed(1)}%` : 'n/a'}
-
- - - -
- - - {currencySymbol} -
- - {showExtensionBidding && ( - <> - -
- - -
- {BIDDING_WINDOWS.map((item) => ( - - ))} -
-
- { - (e.currentTarget as HTMLInputElement).setCustomValidity('Bidding Window is required.'); - }} - onChange={EMPTY_FUNCTION} - /> - - )} - - - - {!form[CheckEnum.START_NOW] ? ( -
-
- - -
- - -
- (e.target as HTMLInputElement).setCustomValidity('Auction hour time is required.')} - maxLength={2} - step="any" - /> - : - { - const num = Number(form['auctionMinute']); - (e.target as HTMLInputElement).setCustomValidity(''); - setForm((form) => ({ ...form, ['auctionMinute']: num >= 10 ? `${num}` : `0${num}` })); - }} - /> -
- - - (e.target as HTMLInputElement).setCustomValidity('Clock format is required.')} - onChange={EMPTY_FUNCTION} - /> -
-
-
- ) : null} - -
-
- - -
- - -
- (e.target as HTMLInputElement).setCustomValidity('Auction hour time is required.')} - maxLength={2} - step="any" - /> - : - { - const num = Number(form['auctionMinuteEnd']); - (e.target as HTMLInputElement).setCustomValidity(''); - setForm((form) => ({ ...form, ['auctionMinuteEnd']: num >= 10 ? `${num}` : `0${num}` })); - }} - /> -
- - -
- (e.target as HTMLInputElement).setCustomValidity('Clock format is required.')} - onChange={EMPTY_FUNCTION} - /> -
-
- -
- - -
- - ); -}; - -const BIDDING_WINDOWS = [ - { label: '3m', value: '180' }, - { label: '5m', value: '300' }, - { label: '10m', value: '600' }, - { label: '15m', value: '900' } -]; diff --git a/core/ui/src/components/Auction/CreateAuctionConfirm/index.tsx b/core/ui/src/components/Auction/Confirm/index.tsx similarity index 87% rename from core/ui/src/components/Auction/CreateAuctionConfirm/index.tsx rename to core/ui/src/components/Auction/Confirm/index.tsx index e3b9d2c6..85a39940 100644 --- a/core/ui/src/components/Auction/CreateAuctionConfirm/index.tsx +++ b/core/ui/src/components/Auction/Confirm/index.tsx @@ -1,11 +1,8 @@ import React from 'react'; import { SingleTokenInfo } from '@liqnft/candy-shop-sdk'; -import { FormType } from '../AuctionForm'; +import { FormType } from '../Form/Form.utils'; import { AuctionNftHeader } from '../AuctionNftHeader'; import { getFormTime } from 'utils/timer'; -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -dayjs.extend(utc); import './style.less'; @@ -43,18 +40,18 @@ export const CreateAuctionConfirm: React.FC = ({ name: 'Auction Start Date', value: getFormTime({ isNow: auctionForm.startNow, - hour: auctionForm.auctionHour, - minute: auctionForm.auctionMinute, - clockFormat: auctionForm.clockFormat, + hour: auctionForm.startHour, + minute: auctionForm.startMinute, + clockFormat: auctionForm.startClockFormat, date: auctionForm.startDate }) }, { name: 'Auction End Date', value: getFormTime({ - hour: auctionForm.auctionHourEnd, - minute: auctionForm.auctionMinuteEnd, - clockFormat: auctionForm.clockFormatEnd, + hour: auctionForm.endHour, + minute: auctionForm.endMinute, + clockFormat: auctionForm.endClockFormat, date: auctionForm.endDate }) } diff --git a/core/ui/src/components/Auction/CreateAuctionConfirm/style.less b/core/ui/src/components/Auction/Confirm/style.less similarity index 100% rename from core/ui/src/components/Auction/CreateAuctionConfirm/style.less rename to core/ui/src/components/Auction/Confirm/style.less diff --git a/core/ui/src/components/Auction/Form/ExtensionBidding.tsx b/core/ui/src/components/Auction/Form/ExtensionBidding.tsx new file mode 100644 index 00000000..b2c02811 --- /dev/null +++ b/core/ui/src/components/Auction/Form/ExtensionBidding.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +import { useFormContext, useFormState, useWatch } from 'react-hook-form'; +import { Checkbox } from 'components/Form'; +import { BIDDING_WINDOWS } from './Form.utils'; + +import './style.less'; + +export const ExtensionBidding = () => { + const { register, setValue } = useFormContext(); + const { errors } = useFormState(); + + const value = useWatch({ name: 'extensionPeriod' }); + const isDisableBiddingExtension = useWatch({ name: 'disableBiddingExtension' }); + + return ( + <> + + + + + ); +}; diff --git a/core/ui/src/components/Auction/Form/Form.utils.ts b/core/ui/src/components/Auction/Form/Form.utils.ts new file mode 100644 index 00000000..da85d98a --- /dev/null +++ b/core/ui/src/components/Auction/Form/Form.utils.ts @@ -0,0 +1,56 @@ +import { SingleTokenInfo } from '@liqnft/candy-shop-sdk'; +import dayjs from 'dayjs'; + +export const BIDDING_WINDOWS = [ + { label: '3m', value: '180' }, + { label: '5m', value: '300' }, + { label: '10m', value: '600' }, + { label: '15m', value: '900' } +]; + +export interface AuctionFormProps { + onSubmit: (...args: any) => void; + currencySymbol?: string; + fee?: number; + nft: SingleTokenInfo; + auctionForm?: FormType; + onBack: () => void; + showExtensionBidding: boolean; +} + +export type FormType = { + startingBid: string; + buyNowPrice: string; + biddingPeriod?: number; + startClockFormat: 'PM' | 'AM'; + startHour: string; + startMinute: string; + endClockFormat: 'PM' | 'AM'; + endHour: string; + endMinute: string; + buyNow?: boolean; + startNow?: boolean; + startDate: string; + endDate: string; + tickSize: string; + disableBiddingExtension: boolean; + extensionPeriod: string; +}; + +export const formDefaultValues: FormType = { + startingBid: '', + tickSize: '', + buyNowPrice: '', + startHour: '12', + startMinute: '00', + startClockFormat: 'AM', + endClockFormat: 'AM', + endHour: '12', + endMinute: '00', + startNow: false, + buyNow: false, + startDate: dayjs().format('YYYY-MM-DD'), + endDate: dayjs().add(1, 'd').format('YYYY-MM-DD'), + disableBiddingExtension: false, + extensionPeriod: '' +}; diff --git a/core/ui/src/components/Auction/Form/TimeFormat.tsx b/core/ui/src/components/Auction/Form/TimeFormat.tsx new file mode 100644 index 00000000..247e4f10 --- /dev/null +++ b/core/ui/src/components/Auction/Form/TimeFormat.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { useFormContext, useWatch } from 'react-hook-form'; + +import './style.less'; + +interface TimeFormatProps { + name: string; + required?: boolean; +} + +export const TimeFormat: React.FC = ({ name, required }) => { + const { setValue, register } = useFormContext(); + + const onClick = (value: 'AM' | 'PM') => () => { + setValue(name, value); + }; + + const timeFormat = useWatch({ name }); + + return ( +
+ + + +
+ ); +}; diff --git a/core/ui/src/components/Auction/Form/index.tsx b/core/ui/src/components/Auction/Form/index.tsx new file mode 100644 index 00000000..7a07dd26 --- /dev/null +++ b/core/ui/src/components/Auction/Form/index.tsx @@ -0,0 +1,140 @@ +import React from 'react'; + +import { AuctionNftHeader } from '../AuctionNftHeader'; + +import { useForm, FormProvider, useWatch } from 'react-hook-form'; +import { InputNumber, Checkbox } from 'components/Form'; +import { Show } from 'components/Show'; +import { DatePicker } from 'components/Form/DatePicker'; +import dayjs from 'dayjs'; +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'; +import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'; +dayjs.extend(isSameOrBefore); +dayjs.extend(isSameOrAfter); +import './style.less'; +import { ExtensionBidding } from './ExtensionBidding'; +import { AuctionFormProps, formDefaultValues, FormType } from './Form.utils'; +import { TimeFormat } from './TimeFormat'; + +export const AuctionForm: React.FC = ({ + onSubmit, + currencySymbol, + fee, + nft, + auctionForm, + onBack, + showExtensionBidding +}) => { + const methods = useForm({ + values: auctionForm || formDefaultValues + }); + const { handleSubmit, setError } = methods; + + const onSubmitForm = (data: FormType) => { + const NOW = dayjs(); + // startDate + + const startDate = data.startNow + ? NOW + : dayjs(data.startDate) + .add((Number(data.startHour) % 12) + (data.startClockFormat === 'PM' ? 12 : 0), 'hour') + .add(Number(data.startMinute), 'minute'); + + if (startDate.isBefore(NOW)) { + return setError('startDate', { message: `Start time must be > current time.` }, { shouldFocus: true }); + } + + if (data.endHour === '12') data.endHour = '0'; + const endDate = dayjs(data.endDate) + .add((Number(data.endHour) % 12) + (data.endClockFormat === 'PM' ? 12 : 0), 'hour') + .add(Number(data.endMinute), 'minute'); + + if (endDate.isSameOrBefore(startDate)) { + return setError('endDate', { message: `End time must be > start time.` }, { shouldFocus: true }); + } + + const biddingPeriod = endDate.diff(startDate, 'second'); + + onSubmit({ ...data, biddingPeriod }); + }; + + const isEnableBuyNow = useWatch({ name: 'buyNow', control: methods.control }); + const isStartNow = useWatch({ name: 'startNow', control: methods.control }); + + return ( + +
+ + + + + +
+
Fees
+
{fee ? `${fee.toFixed(1)}%` : 'n/a'}
+
+ + +
+ ); +}; diff --git a/core/ui/src/components/Auction/AuctionForm/style.less b/core/ui/src/components/Auction/Form/style.less similarity index 95% rename from core/ui/src/components/Auction/AuctionForm/style.less rename to core/ui/src/components/Auction/Form/style.less index 3d7b4675..9d8e5b2d 100644 --- a/core/ui/src/components/Auction/AuctionForm/style.less +++ b/core/ui/src/components/Auction/Form/style.less @@ -18,6 +18,7 @@ /* Firefox */ input[type='number'] { -moz-appearance: textfield; + appearance: textfield; } .candy-auction-form-title { @@ -122,7 +123,8 @@ grid-template-columns: 1fr 5px 1fr 1fr; column-gap: 5px; align-items: center; - margin-bottom: 15px; + margin-bottom: 12px; + margin-top: 12px; > input { width: 100%; @@ -131,6 +133,10 @@ border-radius: 4px; } + .candy-form-input-number { + margin-bottom: 0; + } + .candy-auction-time-checkbox { display: grid; grid-template-columns: 1fr 1fr; diff --git a/core/ui/src/components/Auction/index.ts b/core/ui/src/components/Auction/index.ts index fd178e3b..9cfa6606 100644 --- a/core/ui/src/components/Auction/index.ts +++ b/core/ui/src/components/Auction/index.ts @@ -1,5 +1,5 @@ export * from './AuctionCard'; -export * from './AuctionForm'; +export * from './Form'; export * from './AuctionModal'; export * from './AuctionNftHeader'; -export * from './CreateAuctionConfirm'; +export * from './Confirm'; diff --git a/core/ui/src/components/Checkbox/index.tsx b/core/ui/src/components/Checkbox/index.tsx deleted file mode 100644 index 2d62aa97..00000000 --- a/core/ui/src/components/Checkbox/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; - -import { IconCheck } from 'assets/IconCheck'; - -import './style.less'; - -interface CheckboxProps { - checked: boolean; - label?: string | React.ReactNode; - id: string; - onClick?: any; - className?: string; - disabled?: boolean; -} - -export const Checkbox: React.FC = ({ checked, id, label, onClick, className = '', disabled }) => { - return ( -
-
- - -
- {label && ( - - )} -
- ); -}; diff --git a/core/ui/src/components/Drop/Confirm/index.tsx b/core/ui/src/components/Drop/Confirm/index.tsx index fad62a55..b679f702 100644 --- a/core/ui/src/components/Drop/Confirm/index.tsx +++ b/core/ui/src/components/Drop/Confirm/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { AnchorWallet } from '@solana/wallet-adapter-react'; import { CandyShop, MasterEditionNft } from '@liqnft/candy-shop-sdk'; -import { FormType } from '../Form'; +import { FormType } from '../Form/Form.utils'; import { convertTime12to24, getFormTime } from 'utils/timer'; import dayjs from 'dayjs'; diff --git a/core/ui/src/components/Drop/CreateDrop.tsx b/core/ui/src/components/Drop/CreateDrop.tsx index fd070a91..6d40dfaa 100644 --- a/core/ui/src/components/Drop/CreateDrop.tsx +++ b/core/ui/src/components/Drop/CreateDrop.tsx @@ -3,7 +3,8 @@ import { AnchorWallet } from '@solana/wallet-adapter-react'; import { CandyShop, CANDY_SHOP_V2_PROGRAM_ID, MasterEditionNft } from '@liqnft/candy-shop-sdk'; import { CreateEditionDropConfirm } from 'components/Drop/Confirm'; -import { CreateEditionForm, FormType } from 'components/Drop/Form'; +import { CreateEditionForm } from 'components/Drop/Form'; +import { FormType } from 'components/Drop/Form/Form.utils'; import { DropSelection } from 'components/Drop/Selection'; import './style.less'; @@ -61,6 +62,7 @@ export const CreateDrop: React.FC = ({ walletConnectComponent={walletConnectComponent} /> )} + {stage === DropStage.DETAIL && dropNft ? ( = ({
- {wallet && isShopWithProgramIdV2(candyShop.programId.toString()) - ? ViewStages - : NotProgramIdV2ShopNotification} + {wallet && isShopWithProgramIdV2(candyShop.programId) ? ViewStages : NotProgramIdV2ShopNotification} {!wallet && walletConnectComponent}
diff --git a/core/ui/src/components/Drop/Form/Form.utils.ts b/core/ui/src/components/Drop/Form/Form.utils.ts new file mode 100644 index 00000000..e3699934 --- /dev/null +++ b/core/ui/src/components/Drop/Form/Form.utils.ts @@ -0,0 +1,132 @@ +import { DropUserInputSchema, DROP_USER_INPUT_SCHEMA } from 'constant/drop'; +import dayjs from 'dayjs'; +import IsSameOrBefore from 'dayjs/plugin/isSameOrBefore'; +import IsSameOrAfter from 'dayjs/plugin/isSameOrAfter'; +dayjs.extend(IsSameOrBefore); +dayjs.extend(IsSameOrAfter); + +export const enum FormKey { + name = 'name', + whitelistAddress = 'whitelistAddress', + whitelistTimeFormat = 'whitelistTimeFormat', + whitelistHour = 'whitelistHour', + whitelistMinute = 'whitelistMinute', + whitelistDate = 'whitelistDate', + totalSupply = 'totalSupply', + mintPrice = 'mintPrice', + whitelistRelease = 'whitelistRelease', + salesPeriodZero = 'salesPeriodZero', + saleStartDate = 'saleStartDate', + saleStartHour = 'saleStartHour', + saleStartMinute = 'saleStartMinute', + saleStartTimeFormat = 'saleStartTimeFormat', + saleEndDate = 'saleEndDate', + saleEndHour = 'saleEndHour', + saleEndMinute = 'saleEndMinute', + saleEndTimeFormat = 'saleEndTimeFormat', + description = 'description', + hasRedemption = 'hasRedemption', + redemptionName = 'redemptionName', + redemptionEmail = 'redemptionEmail', + inputSchema = 'inputSchema' +} + +export const enum TimeRowType { + saleStart = 'saleStart', + saleEnd = 'saleEnd', + whitelist = 'whitelist' +} + +export const TimeFormItems: Record< + TimeRowType, + { + dateKey: FormKey; + dateLabel: string; + dateLabelTip: string; + hourLabel: string; + hourKey: FormKey; + minuteKey: FormKey; + timeFormatKey: FormKey; + } +> = { + [TimeRowType.saleStart]: { + dateKey: FormKey.saleStartDate, + dateLabel: 'Sale Start Date', + dateLabelTip: 'Date when buyers can publicly mint from this drop', + hourLabel: 'Sale Start Time', + hourKey: FormKey.saleStartHour, + minuteKey: FormKey.saleStartMinute, + timeFormatKey: FormKey.saleStartTimeFormat + }, + [TimeRowType.saleEnd]: { + dateKey: FormKey.saleEndDate, + dateLabel: 'Sale End Date', + dateLabelTip: 'Date when buyers can no longer mint from this drop', + hourLabel: 'Sale End Time', + hourKey: FormKey.saleEndHour, + minuteKey: FormKey.saleEndMinute, + timeFormatKey: FormKey.saleEndTimeFormat + }, + [TimeRowType.whitelist]: { + dateKey: FormKey.whitelistDate, + dateLabel: 'Whitelist Launch Date', + dateLabelTip: 'Date when whitelisted users can begin mint', + hourLabel: 'Whitelist Launch Time', + hourKey: FormKey.whitelistHour, + minuteKey: FormKey.whitelistMinute, + timeFormatKey: FormKey.whitelistTimeFormat + } +}; + +export type FormType = { + [FormKey.name]: string; + [FormKey.whitelistAddress]: string; + [FormKey.whitelistTimeFormat]: 'AM' | 'PM'; + [FormKey.whitelistHour]: string; + [FormKey.whitelistMinute]: string; + [FormKey.whitelistDate]: string; + [FormKey.totalSupply]: number; + [FormKey.mintPrice]: string; + [FormKey.whitelistRelease]: boolean; + // salesPeriod: string; = end - start + [FormKey.salesPeriodZero]: boolean; + [FormKey.saleStartDate]: string; + [FormKey.saleStartHour]: string; + [FormKey.saleStartMinute]: string; + [FormKey.saleStartTimeFormat]: 'AM' | 'PM'; + [FormKey.saleEndDate]: string; + [FormKey.saleEndHour]: string; + [FormKey.saleEndMinute]: string; + [FormKey.saleEndTimeFormat]: 'AM' | 'PM'; + [FormKey.description]: string; + [FormKey.hasRedemption]: boolean; + [FormKey.redemptionName]: string; + [FormKey.redemptionEmail]: string; + [FormKey.inputSchema]: DropUserInputSchema[]; +}; + +export const formDefaultValue: FormType = { + name: '', + whitelistAddress: '', + whitelistTimeFormat: dayjs().hour() >= 12 ? 'PM' : 'AM', + whitelistHour: '12', + whitelistMinute: dayjs().minute().toString(), + whitelistDate: dayjs().format('YYYY-MM-DD'), + whitelistRelease: false, + totalSupply: 0, + mintPrice: '', + salesPeriodZero: false, + saleStartDate: dayjs().add(1, 'd').format('YYYY-MM-DD'), + saleStartHour: '12', + saleStartMinute: '00', + saleStartTimeFormat: 'AM', + saleEndDate: dayjs().add(2, 'd').format('YYYY-MM-DD'), + saleEndHour: '12', + saleEndMinute: '00', + saleEndTimeFormat: 'AM', + description: '', + hasRedemption: false, + redemptionName: '', + redemptionEmail: '', + inputSchema: DROP_USER_INPUT_SCHEMA.filter((item) => item.required) +}; diff --git a/core/ui/src/components/Drop/Form/RedemptionSetting.tsx b/core/ui/src/components/Drop/Form/RedemptionSetting.tsx new file mode 100644 index 00000000..9651abbc --- /dev/null +++ b/core/ui/src/components/Drop/Form/RedemptionSetting.tsx @@ -0,0 +1,75 @@ +import React, { useEffect } from 'react'; + +import { Checkbox, InputText } from 'components/Form'; +import { FormType, FormKey } from './Form.utils'; + +import { useFormContext, useWatch } from 'react-hook-form'; +import { Show } from 'components/Show'; +import { DROP_USER_INPUT_SCHEMA } from 'constant/drop'; + +import './style.less'; + +export const RedemptionSetting: React.FC = () => { + const hasRedemption = useWatch({ name: FormKey.hasRedemption }); + const inputSchema = useWatch({ name: FormKey.inputSchema }) as FormType['inputSchema']; + const { setValue } = useFormContext(); + + useEffect(() => { + DROP_USER_INPUT_SCHEMA.forEach((item) => setValue(`item-redemption-${item.name}`, true)); + }, [setValue]); + + return ( + <> + + + + + +
+
+ {DROP_USER_INPUT_SCHEMA.map((schema) => { + return ( + + ); + })} +
+
+ Preview + {inputSchema?.map((schema) => { + return ( + + ); + })} + + +
+
+
+ + ); +}; diff --git a/core/ui/src/components/Drop/Form/TimeSelection.tsx b/core/ui/src/components/Drop/Form/TimeSelection.tsx new file mode 100644 index 00000000..f7e75a80 --- /dev/null +++ b/core/ui/src/components/Drop/Form/TimeSelection.tsx @@ -0,0 +1,62 @@ +import React from 'react'; + +import { InputNumber } from 'components/Form'; +import { TimeRowType, TimeFormItems } from './Form.utils'; + +import { useFormContext, useWatch } from 'react-hook-form'; +import { DatePicker } from 'components/Form/DatePicker'; + +import dayjs from 'dayjs'; +import './style.less'; + +interface TimeSelectionProps { + type: TimeRowType; + hidden?: boolean; + disabled?: boolean; +} + +export const TimeSelection: React.FC = ({ type, hidden, disabled }) => { + const rowInfo = TimeFormItems[type]; + + const getFormatTimeButtonClassName = (isActive: boolean) => + `candy-edition-radio ${isActive ? '' : 'candy-edition-radio-disable'}`; + + const { setValue, register } = useFormContext(); + + const onChangeTimeFormat = (format: 'AM' | 'PM') => () => { + setValue(rowInfo.timeFormatKey, format); + }; + const timeFormat = useWatch({ name: rowInfo.timeFormatKey }); + + return ( + + ); +}; diff --git a/core/ui/src/components/Drop/Form/index.tsx b/core/ui/src/components/Drop/Form/index.tsx index 93d759fc..0ff3e29a 100644 --- a/core/ui/src/components/Drop/Form/index.tsx +++ b/core/ui/src/components/Drop/Form/index.tsx @@ -1,14 +1,14 @@ -import React, { useState, useEffect, ReactElement } from 'react'; +import React from 'react'; +import { FormProvider, useForm, useWatch } from 'react-hook-form'; -import { Checkbox } from 'components/Checkbox'; -import { Tooltip } from 'components/Tooltip'; -import { Switch } from 'components/Switch'; -import { InputText } from 'components/Form/Input'; +import { Switch } from 'components/Form/Switch'; +import { Checkbox, InputNumber, InputText } from 'components/Form'; import { MasterEditionNft } from '@liqnft/candy-shop-sdk'; -import { convertTime12to24 } from 'utils/timer'; -import { DropUserInputSchema, DROP_USER_INPUT_SCHEMA } from 'constant/drop'; +import { RedemptionSetting } from './RedemptionSetting'; +import { TimeSelection } from './TimeSelection'; -import { EMPTY_FUNCTION } from 'utils/helperFunc'; +import { Show } from 'components/Show'; +import { FormType, formDefaultValue, FormKey, TimeRowType } from './Form.utils'; import dayjs from 'dayjs'; import IsSameOrBefore from 'dayjs/plugin/isSameOrBefore'; import IsSameOrAfter from 'dayjs/plugin/isSameOrAfter'; @@ -24,118 +24,6 @@ interface CreateEditionFormProps { currencySymbol: string; } -const enum FormKey { - name = 'name', - whitelistAddress = 'whitelistAddress', - whitelistTimeFormat = 'whitelistTimeFormat', - whitelistHour = 'whitelistHour', - whitelistMinute = 'whitelistMinute', - whitelistDate = 'whitelistDate', - totalSupply = 'totalSupply', - mintPrice = 'mintPrice', - whitelistRelease = 'whitelistRelease', - salesPeriodZero = 'salesPeriodZero', - saleStartDate = 'saleStartDate', - saleStartHour = 'saleStartHour', - saleStartMinute = 'saleStartMinute', - saleStartTimeFormat = 'saleStartTimeFormat', - saleEndDate = 'saleEndDate', - saleEndHour = 'saleEndHour', - saleEndMinute = 'saleEndMinute', - saleEndTimeFormat = 'saleEndTimeFormat', - description = 'description', - hasRedemption = 'hasRedemption', - redemptionName = 'redemptionName', - redemptionEmail = 'redemptionEmail', - inputSchema = 'inputSchema' -} - -const enum TimeRowType { - saleStart = 'saleStart', - saleEnd = 'saleEnd', - whitelist = 'whitelist' -} - -const enum TimeFormat { - AM = 'AM', - PM = 'PM' -} - -const TimeFormItems: Record< - TimeRowType, - { - dateKey: FormKey; - dateLabel: string; - dateLabelTip: string; - hourLabel: string; - hourKey: FormKey; - minuteKey: FormKey; - timeFormatKey: FormKey; - } -> = { - [TimeRowType.saleStart]: { - dateKey: FormKey.saleStartDate, - dateLabel: 'Sale Start Date', - dateLabelTip: 'Date when buyers can publicly mint from this drop', - hourLabel: 'Sale Start Time', - hourKey: FormKey.saleStartHour, - minuteKey: FormKey.saleStartMinute, - timeFormatKey: FormKey.saleStartTimeFormat - }, - [TimeRowType.saleEnd]: { - dateKey: FormKey.saleEndDate, - dateLabel: 'Sale End Date', - dateLabelTip: 'Date when buyers can no longer mint from this drop', - hourLabel: 'Sale End Time', - hourKey: FormKey.saleEndHour, - minuteKey: FormKey.saleEndMinute, - timeFormatKey: FormKey.saleEndTimeFormat - }, - [TimeRowType.whitelist]: { - dateKey: FormKey.whitelistDate, - dateLabel: 'Whitelist Launch Date', - dateLabelTip: 'Date when whitelisted users can begin mint', - hourLabel: 'Whitelist Launch Time', - hourKey: FormKey.whitelistHour, - minuteKey: FormKey.whitelistMinute, - timeFormatKey: FormKey.whitelistTimeFormat - } -}; - -export type FormType = { - [FormKey.name]: string; - [FormKey.whitelistAddress]: string; - [FormKey.whitelistTimeFormat]: TimeFormat; - [FormKey.whitelistHour]: string; - [FormKey.whitelistMinute]: string; - [FormKey.whitelistDate]: string; - [FormKey.totalSupply]: number; - [FormKey.mintPrice]: string; - [FormKey.whitelistRelease]: boolean; - // salesPeriod: string; = end - start - [FormKey.salesPeriodZero]: boolean; - [FormKey.saleStartDate]: string; - [FormKey.saleStartHour]: string; - [FormKey.saleStartMinute]: string; - [FormKey.saleStartTimeFormat]: TimeFormat; - [FormKey.saleEndDate]: string; - [FormKey.saleEndHour]: string; - [FormKey.saleEndMinute]: string; - [FormKey.saleEndTimeFormat]: TimeFormat; - [FormKey.description]: string; - [FormKey.hasRedemption]: boolean; - [FormKey.redemptionName]: string; - [FormKey.redemptionEmail]: string; - [FormKey.inputSchema]: DropUserInputSchema[]; -}; - -const validateInput = (nodeId: FormKey, message: string) => { - (document.getElementById(nodeId) as HTMLInputElement)?.setCustomValidity(message); -}; -const reportValidity = () => { - (document.getElementById('edition-form') as HTMLFormElement).reportValidity(); -}; - export const CreateEditionForm: React.FC = ({ onSubmit, nft, @@ -143,439 +31,115 @@ export const CreateEditionForm: React.FC = ({ onBack, currencySymbol }) => { - const [form, setForm] = useState((): Record => { - const getHour = () => { - const hour = dayjs().hour(); - if (hour === 0) return '12'; - if (hour > 12) return (hour - 12).toString(); - return hour.toString(); - }; - - return { - name: nft.name, - whitelistAddress: '', - whitelistTimeFormat: dayjs().hour() >= 12 ? TimeFormat.PM : TimeFormat.AM, - whitelistHour: getHour(), - whitelistMinute: dayjs().minute().toString(), - whitelistDate: dayjs().format('YYYY-MM-DD'), - totalSupply: nft.maxSupply, - mintPrice: '', - whitelistRelease: false, - salesPeriodZero: false, - saleStartDate: dayjs().add(1, 'd').format('YYYY-MM-DD'), - saleStartHour: '12', - saleStartMinute: '00', - saleStartTimeFormat: TimeFormat.AM, - saleEndDate: dayjs().add(2, 'd').format('YYYY-MM-DD'), - saleEndHour: '12', - saleEndMinute: '00', - saleEndTimeFormat: TimeFormat.AM, - description: '', - hasRedemption: false, - redemptionName: '', - redemptionEmail: '', - inputSchema: DROP_USER_INPUT_SCHEMA.filter((item) => item.required) - }; + const methods = useForm({ + defaultValues: formData || { ...formDefaultValue, name: nft.name, totalSupply: nft.maxSupply } }); - const onResetValidation = () => { - validateInput(FormKey.saleStartDate, ''); - validateInput(FormKey.saleEndDate, ''); - validateInput(FormKey.whitelistDate, ''); - }; - - const onCheckTimeFormat = (key: FormKey, value?: any) => (e: any) => { - e.preventDefault(); - onResetValidation(); - setForm((prev: FormType) => ({ ...prev, [key]: value })); - }; - - const onCheckbox = (key: FormKey) => () => { - onResetValidation(); - - setForm((prev: FormType) => ({ ...prev, [key]: !prev[key] })); - }; - - const onCheckInputSchema = (schema: DropUserInputSchema) => () => { - const prevInputSchema = form.inputSchema; - const inputSchema = prevInputSchema.includes(schema) - ? prevInputSchema.filter((n) => n !== schema) - : prevInputSchema.concat(schema); - - setForm((form: FormType) => ({ ...form, inputSchema })); - }; - - const onChangeInput = (e: React.ChangeEvent) => { - const { value, name } = e.target as { value: any; name: FormKey }; - onResetValidation(); - setForm((prev: FormType) => ({ ...prev, [name]: value })); - }; - - const onSubmitForm = (e: any) => { - e.preventDefault(); + const { handleSubmit, setError } = methods; + const onSubmitForm = (data: FormType) => { const NOW = dayjs(); - const startTime = dayjs( - `${form.saleStartDate} ${convertTime12to24(form.saleStartHour, form.saleStartMinute, form.saleStartTimeFormat)}` - ); + const startTime = dayjs(data.saleStartDate) + .add((Number(data.saleStartHour) % 12) + (data.saleStartTimeFormat === 'PM' ? 12 : 0), 'hour') + .add(Number(data.saleStartMinute), 'minute'); - const endTime = dayjs( - `${form.saleEndDate} ${convertTime12to24(form.saleEndHour, form.saleEndMinute, form.saleEndTimeFormat)}` - ); + const endTime = dayjs(data.saleEndDate) + .add((Number(data.saleEndHour) % 12) + (data.saleEndTimeFormat === 'PM' ? 12 : 0), 'hour') + .add(Number(data.saleEndMinute), 'minute'); - if (form.whitelistRelease) { - const whitelistDate = dayjs( - `${form.whitelistDate} ${convertTime12to24(form.whitelistHour, form.whitelistMinute, form.whitelistTimeFormat)}` - ); + if (startTime.isSameOrBefore(NOW)) { + return setError(FormKey.saleStartDate, { message: 'Sale start time must be > current time.' }); + } - if (whitelistDate.isSameOrBefore(NOW)) { - validateInput(FormKey.whitelistDate, 'Whitelist time must be > current time.'); - return reportValidity(); - } + if (data.whitelistRelease) { + const whitelistDate = dayjs(data.whitelistDate) + .add((Number(data.whitelistHour) % 12) + (data.whitelistTimeFormat === 'PM' ? 12 : 0), 'hour') + .add(Number(data.whitelistMinute), 'minute'); if (whitelistDate.isSameOrAfter(startTime)) { - validateInput(FormKey.whitelistDate, 'Whitelist time must be < Sale start time.'); - return reportValidity(); + return setError(FormKey.whitelistDate, { message: 'Whitelist time must be < Sale start time.' }); } - } - if (startTime.isSameOrBefore(NOW)) { - validateInput(FormKey.saleStartDate, 'Sale start time must be > current time.'); - return reportValidity(); + if (whitelistDate.isSameOrBefore(NOW)) { + return setError(FormKey.whitelistDate, { message: 'Whitelist time must be > current time.' }); + } } - if (!form.salesPeriodZero && startTime.isSameOrAfter(endTime)) { - validateInput(FormKey.saleStartDate, 'Sale start time must be < Sale end time.'); - return reportValidity(); + if (!data.salesPeriodZero && startTime.isSameOrAfter(endTime)) { + return setError(FormKey.saleEndDate, { message: 'Sale end time must be > Sale start time.' }); } - onSubmit(form); + onSubmit({ ...data, name: nft.name, totalSupply: nft.maxSupply }); }; - useEffect(() => { - if (formData) setForm(formData); - }, [formData]); + const salesPeriodZero = useWatch({ control: methods.control, name: FormKey.salesPeriodZero }); + const whitelistRelease = useWatch({ control: methods.control, name: FormKey.whitelistRelease }); return ( -
-

Enter details on your new edition drop

- - - - - - - - Sale ends when all NFTs have been sold out - - -
- - {currencySymbol}} - required - value={form[FormKey.mintPrice]} + + +

Enter details on your new edition drop

+ + -
- {/** Redemption view */} - - Item Redemption - - - - - } - /> - -