degmods.com/src/components/ModForm.tsx

840 lines
26 KiB
TypeScript
Raw Normal View History

2024-07-21 21:30:03 +05:00
import _ from 'lodash'
2024-07-25 20:05:28 +05:00
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
2024-08-06 15:46:38 +05:00
import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
2024-07-21 21:30:03 +05:00
import { toast } from 'react-toastify'
import { FixedSizeList as List } from 'react-window'
2024-07-21 21:30:03 +05:00
import { v4 as uuidv4 } from 'uuid'
import { T_TAG_VALUE } from '../constants'
import { useAppSelector, useGames, useNDKContext } from '../hooks'
import { appRoutes, getModPageRoute } from '../routes'
2024-07-12 01:03:52 +05:00
import '../styles/styles.css'
import { DownloadUrl, ModDetails, ModFormState } from '../types'
import {
2024-08-06 15:46:38 +05:00
initializeFormState,
isReachable,
isValidImageUrl,
isValidUrl,
log,
LogType,
now
} from '../utils'
import { CheckboxField, InputError, InputField } from './Inputs'
import { LoadingSpinner } from './LoadingSpinner'
import { NDKEvent } from '@nostr-dev-kit/ndk'
2024-07-21 21:30:03 +05:00
interface FormErrors {
game?: string
title?: string
body?: string
featuredImageUrl?: string
summary?: string
nsfw?: string
screenshotsUrls?: string[]
tags?: string
downloadUrls?: string[]
}
2024-07-21 21:30:03 +05:00
interface GameOption {
value: string
label: string
}
2024-08-06 15:46:38 +05:00
type ModFormProps = {
existingModData?: ModDetails
}
export const ModForm = ({ existingModData }: ModFormProps) => {
const location = useLocation()
2024-07-25 20:05:28 +05:00
const navigate = useNavigate()
const { ndk, publish } = useNDKContext()
const games = useGames()
2024-07-21 21:30:03 +05:00
const userState = useAppSelector((state) => state.user)
const [isPublishing, setIsPublishing] = useState(false)
2024-07-21 21:30:03 +05:00
const [gameOptions, setGameOptions] = useState<GameOption[]>([])
2024-08-06 15:46:38 +05:00
const [formState, setFormState] = useState<ModFormState>(
initializeFormState()
2024-08-06 15:46:38 +05:00
)
const [formErrors, setFormErrors] = useState<FormErrors>({})
2024-08-06 15:46:38 +05:00
useEffect(() => {
if (location.pathname === appRoutes.submitMod) {
setFormState(initializeFormState())
}
}, [location.pathname]) // Only trigger when the pathname changes to submit-mod
useEffect(() => {
if (existingModData) {
setFormState(initializeFormState(existingModData))
}
}, [existingModData])
2024-07-21 21:30:03 +05:00
useEffect(() => {
const options = games.map((game) => ({
label: game['Game Name'],
value: game['Game Name']
}))
setGameOptions(options)
}, [games])
2024-07-21 21:30:03 +05:00
const handleInputChange = useCallback((name: string, value: string) => {
setFormState((prevState) => ({
...prevState,
[name]: value
}))
}, [])
const handleCheckboxChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target
setFormState((prevState) => ({
...prevState,
[name]: checked
}))
},
[]
)
const addScreenshotUrl = useCallback(() => {
setFormState((prevState) => ({
...prevState,
screenshotsUrls: [...prevState.screenshotsUrls, '']
}))
}, [])
const removeScreenshotUrl = useCallback((index: number) => {
setFormState((prevState) => ({
...prevState,
screenshotsUrls: prevState.screenshotsUrls.filter((_, i) => i !== index)
}))
}, [])
const handleScreenshotUrlChange = useCallback(
(index: number, value: string) => {
setFormState((prevState) => ({
...prevState,
screenshotsUrls: [
...prevState.screenshotsUrls.map((url, i) => {
if (index === i) return value
return url
})
]
}))
},
[]
)
const addDownloadUrl = useCallback(() => {
setFormState((prevState) => ({
...prevState,
downloadUrls: [
...prevState.downloadUrls,
{
url: '',
hash: '',
signatureKey: '',
malwareScanLink: '',
modVersion: '',
customNote: ''
}
]
}))
}, [])
const removeDownloadUrl = useCallback((index: number) => {
setFormState((prevState) => ({
...prevState,
downloadUrls: prevState.downloadUrls.filter((_, i) => i !== index)
}))
}, [])
const handleDownloadUrlChange = useCallback(
(index: number, field: keyof DownloadUrl, value: string) => {
setFormState((prevState) => ({
...prevState,
downloadUrls: [
...prevState.downloadUrls.map((url, i) => {
if (index === i) url[field] = value
return url
})
]
}))
},
[]
)
const handlePublish = async () => {
setIsPublishing(true)
2024-07-21 21:30:03 +05:00
let hexPubkey: string
2024-08-14 14:50:28 +05:00
if (userState.auth && userState.user?.pubkey) {
2024-07-21 21:30:03 +05:00
hexPubkey = userState.user.pubkey as string
} else {
hexPubkey = (await window.nostr?.getPublicKey()) as string
}
if (!hexPubkey) {
toast.error('Could not get pubkey')
return
}
if (!(await validateState())) {
setIsPublishing(false)
return
}
2024-07-21 21:30:03 +05:00
2024-11-08 11:02:07 +01:00
const uuid = formState.dTag || uuidv4()
const currentTimeStamp = now()
2024-07-21 21:30:03 +05:00
2024-08-26 17:11:33 +05:00
const aTag =
formState.aTag || `${kinds.ClassifiedListing}:${hexPubkey}:${uuid}`
2024-07-21 21:30:03 +05:00
const unsignedEvent: UnsignedEvent = {
kind: kinds.ClassifiedListing,
created_at: currentTimeStamp,
2024-07-21 21:30:03 +05:00
pubkey: hexPubkey,
content: formState.body,
tags: [
2024-11-08 11:02:07 +01:00
['d', uuid],
2024-08-26 17:11:33 +05:00
['a', aTag],
2024-08-06 15:46:38 +05:00
['r', formState.rTag],
['t', T_TAG_VALUE],
[
'published_at',
existingModData
? existingModData.published_at.toString()
: currentTimeStamp.toString()
],
2024-07-21 21:30:03 +05:00
['game', formState.game],
['title', formState.title],
['featuredImageUrl', formState.featuredImageUrl],
['summary', formState.summary],
['nsfw', formState.nsfw.toString()],
['screenshotsUrls', ...formState.screenshotsUrls],
['tags', ...formState.tags.split(',')],
2024-07-21 21:30:03 +05:00
[
'downloadUrls',
...formState.downloadUrls.map((downloadUrl) =>
JSON.stringify(downloadUrl)
)
]
]
}
const signedEvent = await window.nostr
?.signEvent(unsignedEvent)
2024-07-25 20:05:28 +05:00
.then((event) => event as Event)
.catch((err) => {
toast.error('Failed to sign the event!')
log(true, LogType.Error, 'Failed to sign the event!', err)
return null
})
if (!signedEvent) {
setIsPublishing(false)
return
}
const ndkEvent = new NDKEvent(ndk, signedEvent)
const publishedOnRelays = await publish(ndkEvent)
// Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) {
toast.error('Failed to publish event on any relay')
} else {
toast.success(
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
'\n'
)}`
)
2024-07-25 20:05:28 +05:00
2024-08-26 17:11:33 +05:00
const naddr = nip19.naddrEncode({
identifier: aTag,
pubkey: signedEvent.pubkey,
2024-07-25 20:05:28 +05:00
kind: signedEvent.kind,
relays: publishedOnRelays
})
navigate(getModPageRoute(naddr))
}
2024-07-21 21:30:03 +05:00
setIsPublishing(false)
2024-07-21 21:30:03 +05:00
}
const validateState = async (): Promise<boolean> => {
const errors: FormErrors = {}
2024-07-21 21:30:03 +05:00
if (formState.game === '') {
errors.game = 'Game field can not be empty'
2024-07-21 21:30:03 +05:00
}
if (formState.title === '') {
errors.title = 'Title field can not be empty'
2024-07-21 21:30:03 +05:00
}
if (formState.body === '') {
errors.body = 'Body field can not be empty'
2024-07-21 21:30:03 +05:00
}
if (formState.featuredImageUrl === '') {
errors.featuredImageUrl = 'FeaturedImageUrl field can not be empty'
} else if (
!isValidImageUrl(formState.featuredImageUrl) ||
!(await isReachable(formState.featuredImageUrl))
) {
errors.featuredImageUrl =
'FeaturedImageUrl must be a valid and reachable image URL'
2024-07-21 21:30:03 +05:00
}
if (formState.summary === '') {
errors.summary = 'Summary field can not be empty'
2024-07-21 21:30:03 +05:00
}
if (formState.screenshotsUrls.length === 0) {
errors.screenshotsUrls = ['Required at least one screenshot url']
} else {
for (let i = 0; i < formState.screenshotsUrls.length; i++) {
const url = formState.screenshotsUrls[i]
if (
!isValidUrl(url) ||
!isValidImageUrl(url) ||
!(await isReachable(url))
) {
if (!errors.screenshotsUrls)
errors.screenshotsUrls = Array(formState.screenshotsUrls.length)
errors.screenshotsUrls![i] =
'All screenshot URLs must be valid and reachable image URLs'
}
}
2024-07-21 21:30:03 +05:00
}
if (formState.tags === '') {
errors.tags = 'Tags field can not be empty'
2024-07-21 21:30:03 +05:00
}
if (formState.downloadUrls.length === 0) {
errors.downloadUrls = ['Required at least one download url']
} else {
for (let i = 0; i < formState.downloadUrls.length; i++) {
const downloadUrl = formState.downloadUrls[i]
if (!isValidUrl(downloadUrl.url)) {
if (!errors.downloadUrls)
errors.downloadUrls = Array(formState.downloadUrls.length)
errors.downloadUrls![i] = 'Download url must be valid and reachable'
}
}
2024-07-21 21:30:03 +05:00
}
setFormErrors(errors)
return Object.keys(errors).length === 0
2024-07-21 21:30:03 +05:00
}
2024-07-12 01:03:52 +05:00
return (
<>
{isPublishing && <LoadingSpinner desc='Publishing mod to relays' />}
2024-07-21 21:30:03 +05:00
<GameDropdown
options={gameOptions}
selected={formState.game}
error={formErrors.game}
2024-07-21 21:30:03 +05:00
onChange={handleInputChange}
2024-07-12 01:03:52 +05:00
/>
2024-07-21 21:30:03 +05:00
2024-07-12 01:03:52 +05:00
<InputField
label='Title'
placeholder='Return the banana mod'
name='title'
2024-07-21 21:30:03 +05:00
value={formState.title}
error={formErrors.title}
2024-07-21 21:30:03 +05:00
onChange={handleInputChange}
2024-07-12 01:03:52 +05:00
/>
2024-07-21 21:30:03 +05:00
2024-07-12 01:03:52 +05:00
<InputField
label='Body'
2024-07-21 21:30:03 +05:00
type='richtext'
2024-07-12 01:03:52 +05:00
placeholder="Here's what this mod is all about"
name='body'
2024-07-21 21:30:03 +05:00
value={formState.body}
error={formErrors.body}
2024-07-21 21:30:03 +05:00
onChange={handleInputChange}
2024-07-12 01:03:52 +05:00
/>
2024-07-21 21:30:03 +05:00
2024-07-12 01:03:52 +05:00
<InputField
label='Featured Image URL'
description='We recommend to upload images to https://nostr.build/'
type='text'
inputMode='url'
placeholder='Image URL'
name='featuredImageUrl'
2024-07-21 21:30:03 +05:00
value={formState.featuredImageUrl}
error={formErrors.featuredImageUrl}
2024-07-21 21:30:03 +05:00
onChange={handleInputChange}
2024-07-12 01:03:52 +05:00
/>
<InputField
label='Summary'
type='textarea'
placeholder='This is a quick description of my mod'
name='summary'
2024-07-21 21:30:03 +05:00
value={formState.summary}
error={formErrors.summary}
2024-07-21 21:30:03 +05:00
onChange={handleInputChange}
2024-07-12 01:03:52 +05:00
/>
2024-07-21 21:30:03 +05:00
<CheckboxField
label='This mod not safe for work (NSFW)'
name='nsfw'
isChecked={formState.nsfw}
handleChange={handleCheckboxChange}
2024-10-23 17:23:48 +02:00
type='stylized'
2024-07-12 01:03:52 +05:00
/>
2024-07-21 21:30:03 +05:00
<div className='inputLabelWrapperMain'>
<div className='labelWrapperMain'>
<label className='form-label labelMain'>Screenshots URLs</label>
<button
className='btn btnMain btnMainAdd'
type='button'
onClick={addScreenshotUrl}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-32 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M432 256c0 17.69-14.33 32.01-32 32.01H256v144c0 17.69-14.33 31.99-32 31.99s-32-14.3-32-31.99v-144H48c-17.67 0-32-14.32-32-32.01s14.33-31.99 32-31.99H192v-144c0-17.69 14.33-32.01 32-32.01s32 14.32 32 32.01v144h144C417.7 224 432 238.3 432 256z'></path>
</svg>
</button>
</div>
<p className='labelDescriptionMain'>
We recommend to upload images to https://nostr.build/
</p>
{formState.screenshotsUrls.map((url, index) => (
<Fragment key={`screenShot-${index}`}>
<ScreenshotUrlFields
index={index}
url={url}
onUrlChange={handleScreenshotUrlChange}
onRemove={removeScreenshotUrl}
/>
{formErrors.screenshotsUrls &&
formErrors.screenshotsUrls[index] && (
<InputError message={formErrors.screenshotsUrls[index]} />
)}
2024-08-06 15:46:38 +05:00
</Fragment>
2024-07-21 21:30:03 +05:00
))}
{formState.screenshotsUrls.length === 0 &&
formErrors.screenshotsUrls &&
formErrors.screenshotsUrls[0] && (
<InputError message={formErrors.screenshotsUrls[0]} />
)}
2024-07-21 21:30:03 +05:00
</div>
2024-07-12 01:03:52 +05:00
<InputField
label='Tags'
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
placeholder='Tags'
name='tags'
2024-07-21 21:30:03 +05:00
value={formState.tags}
error={formErrors.tags}
2024-07-21 21:30:03 +05:00
onChange={handleInputChange}
2024-07-12 01:03:52 +05:00
/>
<div className='inputLabelWrapperMain'>
<div className='labelWrapperMain'>
<label className='form-label labelMain'>Download URLs</label>
2024-07-21 21:30:03 +05:00
<button
className='btn btnMain btnMainAdd'
type='button'
onClick={addDownloadUrl}
>
2024-07-12 01:03:52 +05:00
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-32 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M432 256c0 17.69-14.33 32.01-32 32.01H256v144c0 17.69-14.33 31.99-32 31.99s-32-14.3-32-31.99v-144H48c-17.67 0-32-14.32-32-32.01s14.33-31.99 32-31.99H192v-144c0-17.69 14.33-32.01 32-32.01s32 14.32 32 32.01v144h144C417.7 224 432 238.3 432 256z'></path>
</svg>
</button>
</div>
<p className='labelDescriptionMain'>
You can upload your game mod to Github, as an example, and keep
updating it there (another option is catbox.moe). Also, it's advisable
that you hash your package as well with your nostr public key.
2024-07-12 01:03:52 +05:00
</p>
2024-07-21 21:30:03 +05:00
{formState.downloadUrls.map((download, index) => (
<Fragment key={`download-${index}`}>
<DownloadUrlFields
index={index}
url={download.url}
hash={download.hash}
signatureKey={download.signatureKey}
malwareScanLink={download.malwareScanLink}
modVersion={download.modVersion}
customNote={download.customNote}
onUrlChange={handleDownloadUrlChange}
onRemove={removeDownloadUrl}
/>
{formErrors.downloadUrls && formErrors.downloadUrls[index] && (
<InputError message={formErrors.downloadUrls[index]} />
)}
2024-08-06 15:46:38 +05:00
</Fragment>
2024-07-21 21:30:03 +05:00
))}
{formState.downloadUrls.length === 0 &&
formErrors.downloadUrls &&
formErrors.downloadUrls[0] && (
<InputError message={formErrors.downloadUrls[0]} />
)}
2024-07-21 21:30:03 +05:00
</div>
<div className='IBMSMSMBS_WriteAction'>
<button
className='btn btnMain'
type='button'
onClick={handlePublish}
disabled={isPublishing}
>
2024-07-21 21:30:03 +05:00
Publish
</button>
2024-07-12 01:03:52 +05:00
</div>
</>
)
}
2024-07-21 21:30:03 +05:00
type DownloadUrlFieldsProps = {
index: number
url: string
hash: string
signatureKey: string
malwareScanLink: string
modVersion: string
customNote: string
2024-07-21 21:30:03 +05:00
onUrlChange: (index: number, field: keyof DownloadUrl, value: string) => void
onRemove: (index: number) => void
}
2024-07-12 01:03:52 +05:00
2024-07-21 21:30:03 +05:00
const DownloadUrlFields = React.memo(
({
index,
url,
hash,
signatureKey,
malwareScanLink,
modVersion,
customNote,
onUrlChange,
onRemove
}: DownloadUrlFieldsProps) => {
2024-07-21 21:30:03 +05:00
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
onUrlChange(index, name as keyof DownloadUrl, value)
}
return (
<div className='inputWrapperMainWrapper'>
<div className='inputWrapperMain'>
<input
type='text'
className='inputMain'
name='url'
placeholder='Download URL'
value={url}
2024-07-21 21:30:03 +05:00
onChange={handleChange}
/>
<button
className='btn btnMain btnMainRemove'
type='button'
onClick={() => onRemove(index)}
2024-07-12 01:03:52 +05:00
>
2024-07-21 21:30:03 +05:00
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-32 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M135.2 17.69C140.6 6.848 151.7 0 163.8 0H284.2C296.3 0 307.4 6.848 312.8 17.69L320 32H416C433.7 32 448 46.33 448 64C448 81.67 433.7 96 416 96H32C14.33 96 0 81.67 0 64C0 46.33 14.33 32 32 32H128L135.2 17.69zM394.8 466.1C393.2 492.3 372.3 512 346.9 512H101.1C75.75 512 54.77 492.3 53.19 466.1L31.1 128H416L394.8 466.1z'></path>
</svg>
</button>
2024-07-12 01:03:52 +05:00
</div>
2024-07-21 21:30:03 +05:00
<div className='inputWrapperMain'>
<div className='inputWrapperMainBox'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M320 448c0 17.67-14.31 32-32 32H64c-17.69 0-32-14.33-32-32v-384C32 46.34 46.31 32.01 64 32.01S96 46.34 96 64.01v352h192C305.7 416 320 430.3 320 448z'></path>
</svg>
</div>
<input
type='text'
className='inputMain'
name='hash'
placeholder='SHA-256 Hash'
value={hash}
2024-07-21 21:30:03 +05:00
onChange={handleChange}
/>
<div className='inputWrapperMainBox'></div>
</div>
<div className='inputWrapperMain'>
<div className='inputWrapperMainBox'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M320 448c0 17.67-14.31 32-32 32H64c-17.69 0-32-14.33-32-32v-384C32 46.34 46.31 32.01 64 32.01S96 46.34 96 64.01v352h192C305.7 416 320 430.3 320 448z'></path>
</svg>
</div>
<input
type='text'
className='inputMain'
placeholder='Signature public key'
name='signatureKey'
value={signatureKey}
2024-07-21 21:30:03 +05:00
onChange={handleChange}
/>
<div className='inputWrapperMainBox'></div>
</div>
<div className='inputWrapperMain'>
<div className='inputWrapperMainBox'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M320 448c0 17.67-14.31 32-32 32H64c-17.69 0-32-14.33-32-32v-384C32 46.34 46.31 32.01 64 32.01S96 46.34 96 64.01v352h192C305.7 416 320 430.3 320 448z'></path>
</svg>
</div>
<input
type='text'
className='inputMain'
name='malwareScanLink'
placeholder='Malware Scan Link'
value={malwareScanLink}
2024-07-21 21:30:03 +05:00
onChange={handleChange}
/>
<div className='inputWrapperMainBox'></div>
</div>
<div className='inputWrapperMain'>
<div className='inputWrapperMainBox'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M320 448c0 17.67-14.31 32-32 32H64c-17.69 0-32-14.33-32-32v-384C32 46.34 46.31 32.01 64 32.01S96 46.34 96 64.01v352h192C305.7 416 320 430.3 320 448z'></path>
</svg>
</div>
<input
type='text'
className='inputMain'
placeholder='Mod version (1.0)'
name='modVersion'
value={modVersion}
2024-07-21 21:30:03 +05:00
onChange={handleChange}
/>
<div className='inputWrapperMainBox'></div>
</div>
<div className='inputWrapperMain'>
<div className='inputWrapperMainBox'>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M320 448c0 17.67-14.31 32-32 32H64c-17.69 0-32-14.33-32-32v-384C32 46.34 46.31 32.01 64 32.01S96 46.34 96 64.01v352h192C305.7 416 320 430.3 320 448z'></path>
</svg>
</div>
<input
type='text'
className='inputMain'
placeholder='Custom note/message'
name='customNote'
value={customNote}
2024-07-21 21:30:03 +05:00
onChange={handleChange}
/>
<div className='inputWrapperMainBox'></div>
2024-07-12 01:03:52 +05:00
</div>
</div>
2024-07-21 21:30:03 +05:00
)
}
)
type ScreenshotUrlFieldsProps = {
index: number
url: string
onUrlChange: (index: number, value: string) => void
onRemove: (index: number) => void
}
const ScreenshotUrlFields = React.memo(
({ index, url, onUrlChange, onRemove }: ScreenshotUrlFieldsProps) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onUrlChange(index, e.target.value)
}
return (
2024-07-12 01:03:52 +05:00
<div className='inputWrapperMain'>
<input
type='text'
className='inputMain'
inputMode='url'
2024-07-21 21:30:03 +05:00
placeholder='We recommend to upload images to https://nostr.build/'
value={url}
onChange={handleChange}
2024-07-12 01:03:52 +05:00
/>
2024-07-21 21:30:03 +05:00
<button
className='btn btnMain btnMainRemove'
type='button'
onClick={() => onRemove(index)}
>
2024-07-12 01:03:52 +05:00
<svg
xmlns='http://www.w3.org/2000/svg'
2024-07-21 21:30:03 +05:00
viewBox='-32 0 512 512'
2024-07-12 01:03:52 +05:00
width='1em'
height='1em'
fill='currentColor'
>
2024-07-21 21:30:03 +05:00
<path d='M323.3 32.01H188.7C172.3 32.01 160 44.31 160 60.73V96.01H32C14.33 96.01 0 110.3 0 128S14.33 160 32 160H480c17.67 0 32-14.33 32-32.01S497.7 96.01 480 96.01H352v-35.28C352 44.31 339.7 32.01 323.3 32.01zM64.9 477.5C66.5 492.3 79.31 504 94.72 504H417.3c15.41 0 28.22-11.72 29.81-26.5L480 192.2H32L64.9 477.5z'></path>
2024-07-12 01:03:52 +05:00
</svg>
2024-07-21 21:30:03 +05:00
</button>
</div>
)
}
)
type GameDropdownProps = {
options: GameOption[]
selected: string
error?: string
2024-07-21 21:30:03 +05:00
onChange: (name: string, value: string) => void
}
const GameDropdown = ({
options,
selected,
error,
onChange
}: GameDropdownProps) => {
2024-07-21 21:30:03 +05:00
const [searchTerm, setSearchTerm] = useState('')
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('')
const inputRef = useRef<HTMLInputElement>(null)
const isOptionClicked = useRef(false)
const handleSearchChange = useCallback((value: string) => {
setSearchTerm(value)
_.debounce(() => {
setDebouncedSearchTerm(value)
}, 300)()
}, [])
const filteredOptions = useMemo(() => {
if (debouncedSearchTerm === '') return []
else {
return options.filter((option) =>
option.label.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
)
}
}, [debouncedSearchTerm, options])
return (
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Game</label>
<p className='labelDescriptionMain'>
Can't find the game you're looking for? You can temporarily publish the
mod under '(Unlisted Game)' and later edit it with the proper game name
once we add it.
2024-07-21 21:30:03 +05:00
</p>
<div className='dropdown dropdownMain'>
<div className='inputWrapperMain inputWrapperMainAlt'>
<input
ref={inputRef}
type='text'
className='inputMain inputMainWithBtn dropdown-toggle'
placeholder='This mod is for a game called...'
aria-expanded='false'
data-bs-toggle='dropdown'
value={searchTerm || selected}
onChange={(e) => handleSearchChange(e.target.value)}
onBlur={() => {
if (!isOptionClicked.current) {
setSearchTerm('')
}
isOptionClicked.current = false
}}
/>
<button
className='btn btnMain btnMainInsideField btnMainRemove'
type='button'
onClick={() => onChange('game', '')}
2024-07-12 01:03:52 +05:00
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z'></path>
</svg>
</button>
<div className='dropdown-menu dropdownMainMenu dropdownMainMenuAlt'>
<List
height={500}
width={'100%'}
itemCount={filteredOptions.length}
itemSize={35}
>
{({ index, style }) => (
<div
style={style}
className='dropdown-item dropdownMainMenuItem'
onMouseDown={() => (isOptionClicked.current = true)}
onClick={() => {
onChange('game', filteredOptions[index].value)
setSearchTerm('')
if (inputRef.current) {
inputRef.current.blur()
}
}}
>
{filteredOptions[index].label}
</div>
)}
</List>
</div>
2024-07-12 01:03:52 +05:00
</div>
</div>
2024-10-19 07:47:42 +00:00
{error && <InputError message={error} />}
<p className='labelDescriptionMain'>
Note: Please mention the game name in the body text of your mod post
(e.g., 'This is a mod for Game Name') so we know what to look for and
add.
2024-10-19 07:43:20 +00:00
</p>
2024-07-12 01:03:52 +05:00
</div>
)
}