2024-07-21 21:30:03 +05:00
|
|
|
import _ from 'lodash'
|
|
|
|
import Papa from 'papaparse'
|
|
|
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
|
import 'react-quill/dist/quill.snow.css'
|
|
|
|
import { FixedSizeList as List } from 'react-window'
|
|
|
|
|
|
|
|
import { kinds, UnsignedEvent } from 'nostr-tools'
|
|
|
|
import { toast } from 'react-toastify'
|
|
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
import { useAppSelector } from '../hooks'
|
2024-07-12 01:03:52 +05:00
|
|
|
import '../styles/styles.css'
|
2024-07-21 21:30:03 +05:00
|
|
|
import { now } from '../utils'
|
|
|
|
import { CheckboxField, InputField } from './Inputs'
|
|
|
|
|
|
|
|
interface DownloadUrl {
|
|
|
|
url: string
|
|
|
|
hash: string
|
|
|
|
signatureKey: string
|
|
|
|
malwareScanLink: string
|
|
|
|
modVersion: string
|
|
|
|
customNote: string
|
|
|
|
}
|
|
|
|
|
|
|
|
interface FormState {
|
|
|
|
game: string
|
|
|
|
title: string
|
|
|
|
body: string
|
|
|
|
featuredImageUrl: string
|
|
|
|
summary: string
|
|
|
|
nsfw: boolean
|
|
|
|
screenshotsUrls: string[]
|
|
|
|
tags: string
|
|
|
|
downloadUrls: DownloadUrl[]
|
|
|
|
}
|
|
|
|
|
|
|
|
interface GameOption {
|
|
|
|
value: string
|
|
|
|
label: string
|
|
|
|
}
|
|
|
|
|
|
|
|
let processedCSV = false
|
2024-07-12 01:03:52 +05:00
|
|
|
|
|
|
|
export const ModForm = () => {
|
2024-07-21 21:30:03 +05:00
|
|
|
const userState = useAppSelector((state) => state.user)
|
|
|
|
|
|
|
|
const [gameOptions, setGameOptions] = useState<GameOption[]>([])
|
|
|
|
|
|
|
|
const [formState, setFormState] = useState<FormState>({
|
|
|
|
game: '',
|
|
|
|
title: '',
|
|
|
|
body: '',
|
|
|
|
featuredImageUrl: '',
|
|
|
|
summary: '',
|
|
|
|
nsfw: false,
|
|
|
|
screenshotsUrls: [''],
|
|
|
|
tags: '',
|
|
|
|
downloadUrls: [
|
|
|
|
{
|
|
|
|
url: '',
|
|
|
|
hash: '',
|
|
|
|
signatureKey: '',
|
|
|
|
malwareScanLink: '',
|
|
|
|
modVersion: '',
|
|
|
|
customNote: ''
|
|
|
|
}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (processedCSV) return
|
|
|
|
processedCSV = true
|
|
|
|
|
|
|
|
// Fetch the CSV file from the public folder
|
|
|
|
fetch('/assets/games.csv')
|
|
|
|
.then((response) => response.text())
|
|
|
|
.then((csvText) => {
|
|
|
|
// Parse the CSV text using PapaParse
|
|
|
|
Papa.parse<{
|
|
|
|
'Game Name': string
|
|
|
|
'16 by 9 image': string
|
|
|
|
'Boxart image': string
|
|
|
|
}>(csvText, {
|
|
|
|
worker: true,
|
|
|
|
header: true,
|
|
|
|
complete: (results) => {
|
|
|
|
const options = results.data.map((row) => ({
|
|
|
|
label: row['Game Name'],
|
|
|
|
value: row['Game Name']
|
|
|
|
}))
|
|
|
|
setGameOptions(options)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.catch((error) => console.error('Error fetching CSV file:', error))
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
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 () => {
|
|
|
|
let hexPubkey: string
|
|
|
|
|
|
|
|
if (userState.isAuth && userState.user?.pubkey) {
|
|
|
|
hexPubkey = userState.user.pubkey as string
|
|
|
|
} else {
|
|
|
|
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!hexPubkey) {
|
|
|
|
toast.error('Could not get pubkey')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!validateState()) return
|
|
|
|
|
|
|
|
const uuid = uuidv4()
|
|
|
|
|
|
|
|
const unsignedEvent: UnsignedEvent = {
|
|
|
|
kind: kinds.ClassifiedListing,
|
|
|
|
created_at: now(),
|
|
|
|
pubkey: hexPubkey,
|
|
|
|
content: formState.body,
|
|
|
|
tags: [
|
|
|
|
['d', uuid],
|
|
|
|
// todo: t tag for degmod identifier (use url)
|
|
|
|
['game', formState.game],
|
|
|
|
['title', formState.title],
|
|
|
|
['featuredImageUrl', formState.featuredImageUrl],
|
|
|
|
['summary', formState.summary],
|
|
|
|
['nsfw', formState.nsfw.toString()],
|
|
|
|
['screenshotsUrls', ...formState.screenshotsUrls],
|
|
|
|
['tags', formState.tags],
|
|
|
|
[
|
|
|
|
'downloadUrls',
|
|
|
|
...formState.downloadUrls.map((downloadUrl) =>
|
|
|
|
JSON.stringify(downloadUrl)
|
|
|
|
)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
const signedEvent = await window.nostr?.signEvent(unsignedEvent)
|
|
|
|
console.log('signedEvent :>> ', signedEvent)
|
|
|
|
|
|
|
|
// todo: publish event
|
|
|
|
}
|
|
|
|
|
|
|
|
const validateState = () => {
|
|
|
|
if (formState.game === '') {
|
|
|
|
toast.error('Game field can not be empty')
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formState.title === '') {
|
|
|
|
toast.error('Title field can not be empty')
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formState.body === '') {
|
|
|
|
toast.error('Body field can not be empty')
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formState.featuredImageUrl === '') {
|
|
|
|
toast.error('FeaturedImageUrl field can not be empty')
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formState.summary === '') {
|
|
|
|
toast.error('Summary field can not be empty')
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
formState.screenshotsUrls.length === 0 ||
|
|
|
|
formState.screenshotsUrls.every((url) => url === '')
|
|
|
|
) {
|
|
|
|
toast.error('Required at least one screenshot url')
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (formState.tags === '') {
|
|
|
|
toast.error('tags field can not be empty')
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
formState.downloadUrls.length === 0 ||
|
|
|
|
formState.downloadUrls.every((download) => download.url === '')
|
|
|
|
) {
|
|
|
|
toast.error('Required at least one download url')
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2024-07-12 01:03:52 +05:00
|
|
|
return (
|
|
|
|
<>
|
2024-07-21 21:30:03 +05:00
|
|
|
<GameDropdown
|
|
|
|
options={gameOptions}
|
|
|
|
selected={formState.game}
|
|
|
|
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}
|
|
|
|
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}
|
|
|
|
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}
|
|
|
|
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}
|
|
|
|
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-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) => (
|
|
|
|
<ScreenshotUrlFields
|
|
|
|
key={index}
|
|
|
|
index={index}
|
|
|
|
url={url}
|
|
|
|
onUrlChange={handleScreenshotUrlChange}
|
|
|
|
onRemove={removeScreenshotUrl}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</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}
|
|
|
|
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
|
2024-07-21 21:30:03 +05:00
|
|
|
updating it there. Also, it's advisable that you hash your package as
|
2024-07-12 01:03:52 +05:00
|
|
|
well with your nostr public key.
|
|
|
|
</p>
|
|
|
|
|
2024-07-21 21:30:03 +05:00
|
|
|
{formState.downloadUrls.map((download, index) => (
|
|
|
|
<DownloadUrlFields
|
|
|
|
key={index}
|
|
|
|
index={index}
|
|
|
|
download={download}
|
|
|
|
onUrlChange={handleDownloadUrlChange}
|
|
|
|
onRemove={removeDownloadUrl}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
<div className='IBMSMSMBS_WriteAction'>
|
|
|
|
<button className='btn btnMain' type='button' onClick={handlePublish}>
|
|
|
|
Publish
|
|
|
|
</button>
|
2024-07-12 01:03:52 +05:00
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
2024-07-21 21:30:03 +05:00
|
|
|
type DownloadUrlFieldsProps = {
|
|
|
|
index: number
|
|
|
|
download: DownloadUrl
|
|
|
|
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, download, onUrlChange, onRemove }: DownloadUrlFieldsProps) => {
|
|
|
|
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={download.url}
|
|
|
|
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={download.hash}
|
|
|
|
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={download.signatureKey}
|
|
|
|
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={download.malwareScanLink}
|
|
|
|
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={download.modVersion}
|
|
|
|
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={download.customNote}
|
|
|
|
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
|
|
|
|
onChange: (name: string, value: string) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
const GameDropdown = ({ options, selected, onChange }: GameDropdownProps) => {
|
|
|
|
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? Send us a DM mentioning it so we
|
|
|
|
can add it.
|
|
|
|
</p>
|
|
|
|
<div className='dropdown dropdownMain'>
|
2024-07-12 01:03:52 +05:00
|
|
|
<input
|
2024-07-21 21:30:03 +05:00
|
|
|
ref={inputRef}
|
2024-07-12 01:03:52 +05:00
|
|
|
type='text'
|
2024-07-21 21:30:03 +05:00
|
|
|
className='inputMain 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
|
|
|
|
}}
|
2024-07-12 01:03:52 +05:00
|
|
|
/>
|
2024-07-21 21:30:03 +05:00
|
|
|
<div className='dropdown-menu dropdownMainMenu'>
|
|
|
|
<List
|
|
|
|
height={500}
|
|
|
|
width={'100%'}
|
|
|
|
itemCount={filteredOptions.length}
|
|
|
|
itemSize={35}
|
2024-07-12 01:03:52 +05:00
|
|
|
>
|
2024-07-21 21:30:03 +05:00
|
|
|
{({ 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>
|
2024-07-12 01:03:52 +05:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|