2024-07-21 21:30:03 +05:00
|
|
|
import _ from 'lodash'
|
2024-08-06 15:46:38 +05:00
|
|
|
import React, {
|
|
|
|
Fragment,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useMemo,
|
|
|
|
useRef,
|
|
|
|
useState
|
|
|
|
} from 'react'
|
2024-12-23 20:21:06 +01:00
|
|
|
import {
|
|
|
|
useActionData,
|
|
|
|
useLoaderData,
|
|
|
|
useNavigation,
|
|
|
|
useSubmit
|
|
|
|
} from 'react-router-dom'
|
2024-12-11 14:03:33 +01:00
|
|
|
import { FixedSizeList } from 'react-window'
|
2024-12-23 20:21:06 +01:00
|
|
|
import { useGames } from '../hooks'
|
2024-07-12 01:03:52 +05:00
|
|
|
import '../styles/styles.css'
|
2024-07-24 15:13:28 +05:00
|
|
|
import {
|
2024-12-23 20:21:06 +01:00
|
|
|
DownloadUrl,
|
|
|
|
FormErrors,
|
|
|
|
ModFormState,
|
|
|
|
ModPageLoaderResult
|
|
|
|
} from '../types'
|
|
|
|
import { initializeFormState } from '../utils'
|
2025-01-07 09:49:02 +01:00
|
|
|
import { CheckboxField, InputField, InputFieldWithImageUpload } from './Inputs'
|
2024-11-27 17:17:54 +01:00
|
|
|
import { OriginalAuthor } from './OriginalAuthor'
|
2024-12-12 14:20:57 +01:00
|
|
|
import { CategoryAutocomplete } from './CategoryAutocomplete'
|
2024-12-12 16:19:08 +01:00
|
|
|
import { AlertPopup } from './AlertPopup'
|
2024-12-23 20:21:06 +01:00
|
|
|
import { Editor, EditorRef } from './Markdown/Editor'
|
2025-01-07 09:49:02 +01:00
|
|
|
import { MEDIA_OPTIONS } from 'controllers'
|
|
|
|
import { InputError } from './Inputs/Error'
|
2025-01-07 12:40:27 +01:00
|
|
|
import { ImageUpload } from './Inputs/ImageUpload'
|
2024-07-24 15:13:28 +05:00
|
|
|
|
2024-07-21 21:30:03 +05:00
|
|
|
interface GameOption {
|
|
|
|
value: string
|
|
|
|
label: string
|
|
|
|
}
|
|
|
|
|
2024-12-23 20:21:06 +01:00
|
|
|
export const ModForm = () => {
|
|
|
|
const data = useLoaderData() as ModPageLoaderResult
|
|
|
|
const mod = data?.mod
|
|
|
|
const formErrors = useActionData() as FormErrors
|
|
|
|
const navigation = useNavigation()
|
|
|
|
const submit = useSubmit()
|
2024-09-18 21:33:22 +05:00
|
|
|
const games = useGames()
|
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>(
|
2024-12-23 20:21:06 +01:00
|
|
|
initializeFormState(mod)
|
2024-08-06 15:46:38 +05:00
|
|
|
)
|
2025-01-07 12:40:27 +01:00
|
|
|
console.log(`[debug] screenshots`, formState.screenshotsUrls)
|
2024-12-23 20:21:06 +01:00
|
|
|
const editorRef = useRef<EditorRef>(null)
|
2024-09-30 14:52:19 +05:00
|
|
|
|
2024-07-21 21:30:03 +05:00
|
|
|
useEffect(() => {
|
2024-09-18 21:33:22 +05:00
|
|
|
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
|
|
|
|
})
|
|
|
|
]
|
|
|
|
}))
|
|
|
|
},
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
|
2024-12-12 16:19:08 +01:00
|
|
|
const [showConfirmPopup, setShowConfirmPopup] = useState<boolean>(false)
|
|
|
|
const handleReset = () => {
|
|
|
|
setShowConfirmPopup(true)
|
|
|
|
}
|
|
|
|
const handleResetConfirm = (confirm: boolean) => {
|
|
|
|
setShowConfirmPopup(false)
|
|
|
|
|
|
|
|
// Cancel if not confirmed
|
|
|
|
if (!confirm) return
|
|
|
|
|
|
|
|
// Editing
|
2024-12-23 20:21:06 +01:00
|
|
|
if (mod) {
|
|
|
|
const initial = initializeFormState(mod)
|
|
|
|
|
|
|
|
// Reset editor
|
|
|
|
editorRef.current?.setMarkdown(initial.body)
|
|
|
|
|
2024-12-12 16:19:08 +01:00
|
|
|
// Reset fields to the original existing data
|
2024-12-23 20:21:06 +01:00
|
|
|
setFormState(initial)
|
2024-12-12 16:19:08 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// New - set form state to the initial (clear form state)
|
|
|
|
setFormState(initializeFormState())
|
|
|
|
}
|
2024-12-23 20:21:06 +01:00
|
|
|
const handlePublish = () => {
|
|
|
|
submit(JSON.stringify(formState), {
|
|
|
|
method: mod ? 'put' : 'post',
|
|
|
|
encType: 'application/json'
|
|
|
|
})
|
2024-07-21 21:30:03 +05:00
|
|
|
}
|
|
|
|
|
2024-07-12 01:03:52 +05:00
|
|
|
return (
|
2024-12-23 20:21:06 +01:00
|
|
|
<form
|
|
|
|
className='IBMSMSMBS_Write'
|
|
|
|
onSubmit={(e) => {
|
|
|
|
e.preventDefault()
|
|
|
|
handlePublish()
|
|
|
|
}}
|
|
|
|
>
|
2024-07-21 21:30:03 +05:00
|
|
|
<GameDropdown
|
|
|
|
options={gameOptions}
|
2024-12-23 20:21:06 +01:00
|
|
|
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}
|
2024-12-23 20:21:06 +01:00
|
|
|
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-12-23 20:21:06 +01:00
|
|
|
<div className='inputLabelWrapperMain'>
|
|
|
|
<label className='form-label labelMain'>Body</label>
|
|
|
|
<div className='inputMain'>
|
|
|
|
<Editor
|
|
|
|
ref={editorRef}
|
2024-12-24 20:20:13 +01:00
|
|
|
markdown={formState.body}
|
2024-12-23 20:21:06 +01:00
|
|
|
placeholder="Here's what this mod is all about"
|
|
|
|
onChange={(md) => {
|
|
|
|
handleInputChange('body', md)
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
{typeof formErrors?.body !== 'undefined' && (
|
|
|
|
<InputError message={formErrors?.body} />
|
|
|
|
)}
|
|
|
|
<input
|
|
|
|
name='body'
|
|
|
|
hidden
|
|
|
|
value={encodeURIComponent(formState?.body)}
|
|
|
|
readOnly
|
|
|
|
/>
|
|
|
|
</div>
|
2024-07-21 21:30:03 +05:00
|
|
|
|
2025-01-07 09:49:02 +01:00
|
|
|
<InputFieldWithImageUpload
|
2024-07-12 01:03:52 +05:00
|
|
|
label='Featured Image URL'
|
2025-01-07 09:49:02 +01:00
|
|
|
description={`We recommend to upload images to ${MEDIA_OPTIONS[0].host}`}
|
2024-07-12 01:03:52 +05:00
|
|
|
inputMode='url'
|
|
|
|
placeholder='Image URL'
|
|
|
|
name='featuredImageUrl'
|
2024-07-21 21:30:03 +05:00
|
|
|
value={formState.featuredImageUrl}
|
2024-12-23 20:21:06 +01:00
|
|
|
error={formErrors?.featuredImageUrl}
|
2025-01-07 12:40:27 +01:00
|
|
|
onInputChange={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}
|
2024-12-23 20:21:06 +01:00
|
|
|
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-11-27 17:17:54 +01:00
|
|
|
<CheckboxField
|
|
|
|
label='This is a repost of a mod I did not create'
|
|
|
|
name='repost'
|
|
|
|
isChecked={formState.repost}
|
|
|
|
handleChange={handleCheckboxChange}
|
|
|
|
type='stylized'
|
|
|
|
/>
|
|
|
|
{formState.repost && (
|
|
|
|
<>
|
|
|
|
<InputField
|
|
|
|
label={
|
|
|
|
<span>
|
|
|
|
Created by:{' '}
|
|
|
|
{<OriginalAuthor value={formState.originalAuthor || ''} />}
|
|
|
|
</span>
|
|
|
|
}
|
|
|
|
type='text'
|
|
|
|
placeholder="Original author's name, npub or nprofile"
|
|
|
|
name='originalAuthor'
|
|
|
|
value={formState.originalAuthor || ''}
|
2024-12-23 20:21:06 +01:00
|
|
|
error={formErrors?.originalAuthor}
|
2024-11-27 17:17:54 +01:00
|
|
|
onChange={handleInputChange}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
)}
|
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}
|
2024-12-12 14:20:57 +01:00
|
|
|
title='Add'
|
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='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'>
|
2025-01-07 12:40:27 +01:00
|
|
|
We recommend to upload images to {MEDIA_OPTIONS[0].host}
|
2024-07-21 21:30:03 +05:00
|
|
|
</p>
|
2025-01-07 12:40:27 +01:00
|
|
|
|
|
|
|
<ImageUpload
|
|
|
|
multiple={true}
|
|
|
|
onChange={(values) => {
|
|
|
|
// values.forEach((screenshotUrl) => addScreenshotUrl())
|
|
|
|
setFormState((prevState) => ({
|
|
|
|
...prevState,
|
|
|
|
screenshotsUrls: Array.from(
|
|
|
|
new Set([
|
|
|
|
...prevState.screenshotsUrls.filter((url) => url),
|
|
|
|
...values
|
|
|
|
])
|
|
|
|
)
|
|
|
|
}))
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
|
2024-07-21 21:30:03 +05:00
|
|
|
{formState.screenshotsUrls.map((url, index) => (
|
2024-10-17 17:43:46 +02:00
|
|
|
<Fragment key={`screenShot-${index}`}>
|
2024-07-24 15:13:28 +05:00
|
|
|
<ScreenshotUrlFields
|
|
|
|
index={index}
|
|
|
|
url={url}
|
|
|
|
onUrlChange={handleScreenshotUrlChange}
|
|
|
|
onRemove={removeScreenshotUrl}
|
|
|
|
/>
|
2024-12-23 20:21:06 +01:00
|
|
|
{formErrors?.screenshotsUrls &&
|
|
|
|
formErrors?.screenshotsUrls[index] && (
|
|
|
|
<InputError message={formErrors?.screenshotsUrls[index]} />
|
2024-07-24 15:13:28 +05:00
|
|
|
)}
|
2024-08-06 15:46:38 +05:00
|
|
|
</Fragment>
|
2024-07-21 21:30:03 +05:00
|
|
|
))}
|
2024-07-24 15:13:28 +05:00
|
|
|
{formState.screenshotsUrls.length === 0 &&
|
2024-12-23 20:21:06 +01:00
|
|
|
formErrors?.screenshotsUrls &&
|
|
|
|
formErrors?.screenshotsUrls[0] && (
|
|
|
|
<InputError message={formErrors?.screenshotsUrls[0]} />
|
2024-07-24 15:13:28 +05:00
|
|
|
)}
|
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}
|
2024-12-23 20:21:06 +01:00
|
|
|
error={formErrors?.tags}
|
2024-07-21 21:30:03 +05:00
|
|
|
onChange={handleInputChange}
|
2024-07-12 01:03:52 +05:00
|
|
|
/>
|
2024-12-03 10:47:55 +01:00
|
|
|
<CategoryAutocomplete
|
2024-12-03 19:27:51 +01:00
|
|
|
game={formState.game}
|
2024-12-03 10:47:55 +01:00
|
|
|
LTags={formState.LTags}
|
|
|
|
setFormState={setFormState}
|
|
|
|
/>
|
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-12-12 14:20:57 +01:00
|
|
|
title='Add'
|
2024-07-21 21:30:03 +05:00
|
|
|
>
|
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-10-17 17:43:46 +02:00
|
|
|
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) => (
|
2024-10-17 17:43:46 +02:00
|
|
|
<Fragment key={`download-${index}`}>
|
2024-07-24 15:13:28 +05:00
|
|
|
<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}
|
|
|
|
/>
|
2024-12-23 20:21:06 +01:00
|
|
|
{formErrors?.downloadUrls && formErrors?.downloadUrls[index] && (
|
|
|
|
<InputError message={formErrors?.downloadUrls[index]} />
|
2024-07-24 15:13:28 +05:00
|
|
|
)}
|
2024-08-06 15:46:38 +05:00
|
|
|
</Fragment>
|
2024-07-21 21:30:03 +05:00
|
|
|
))}
|
2024-07-24 15:13:28 +05:00
|
|
|
{formState.downloadUrls.length === 0 &&
|
2024-12-23 20:21:06 +01:00
|
|
|
formErrors?.downloadUrls &&
|
|
|
|
formErrors?.downloadUrls[0] && (
|
|
|
|
<InputError message={formErrors?.downloadUrls[0]} />
|
2024-07-24 15:13:28 +05:00
|
|
|
)}
|
2024-07-21 21:30:03 +05:00
|
|
|
</div>
|
|
|
|
<div className='IBMSMSMBS_WriteAction'>
|
2024-12-12 16:19:08 +01:00
|
|
|
<button
|
|
|
|
className='btn btnMain'
|
|
|
|
type='button'
|
|
|
|
onClick={handleReset}
|
2024-12-23 20:21:06 +01:00
|
|
|
disabled={
|
|
|
|
navigation.state === 'loading' || navigation.state === 'submitting'
|
|
|
|
}
|
2024-12-12 16:19:08 +01:00
|
|
|
>
|
2024-12-23 20:21:06 +01:00
|
|
|
{mod ? 'Reset' : 'Clear fields'}
|
2024-12-12 16:19:08 +01:00
|
|
|
</button>
|
2024-07-24 15:13:28 +05:00
|
|
|
<button
|
|
|
|
className='btn btnMain'
|
2024-12-23 20:21:06 +01:00
|
|
|
type='submit'
|
|
|
|
disabled={
|
|
|
|
navigation.state === 'loading' || navigation.state === 'submitting'
|
|
|
|
}
|
2024-07-24 15:13:28 +05:00
|
|
|
>
|
2024-12-23 20:21:06 +01:00
|
|
|
{navigation.state === 'submitting' ? 'Publishing...' : 'Publish'}
|
2024-07-21 21:30:03 +05:00
|
|
|
</button>
|
2024-07-12 01:03:52 +05:00
|
|
|
</div>
|
2024-12-12 16:19:08 +01:00
|
|
|
{showConfirmPopup && (
|
|
|
|
<AlertPopup
|
|
|
|
handleConfirm={handleResetConfirm}
|
|
|
|
handleClose={() => setShowConfirmPopup(false)}
|
|
|
|
header={'Are you sure?'}
|
|
|
|
label={
|
2024-12-23 20:21:06 +01:00
|
|
|
mod
|
2024-12-12 16:19:08 +01:00
|
|
|
? `Are you sure you want to clear all changes?`
|
|
|
|
: `Are you sure you want to clear all field data?`
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
)}
|
2024-12-23 20:21:06 +01:00
|
|
|
</form>
|
2024-07-12 01:03:52 +05:00
|
|
|
)
|
|
|
|
}
|
2024-07-21 21:30:03 +05:00
|
|
|
type DownloadUrlFieldsProps = {
|
|
|
|
index: number
|
2024-07-24 15:13:28 +05:00
|
|
|
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(
|
2024-07-24 15:13:28 +05:00
|
|
|
({
|
|
|
|
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'
|
2024-07-24 15:13:28 +05:00
|
|
|
value={url}
|
2024-07-21 21:30:03 +05:00
|
|
|
onChange={handleChange}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<button
|
|
|
|
className='btn btnMain btnMainRemove'
|
|
|
|
type='button'
|
|
|
|
onClick={() => onRemove(index)}
|
2024-12-12 14:20:57 +01:00
|
|
|
title='Remove'
|
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'
|
2024-07-24 15:13:28 +05:00
|
|
|
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'
|
2024-07-24 15:13:28 +05:00
|
|
|
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'
|
2024-07-24 15:13:28 +05:00
|
|
|
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'
|
2024-07-24 15:13:28 +05:00
|
|
|
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'
|
2024-07-24 15:13:28 +05:00
|
|
|
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'
|
2025-01-07 12:40:27 +01:00
|
|
|
placeholder='Image URL'
|
2024-07-21 21:30:03 +05:00
|
|
|
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-12-12 14:20:57 +01:00
|
|
|
title='Remove'
|
2024-07-21 21:30:03 +05:00
|
|
|
>
|
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
|
2024-07-24 15:13:28 +05:00
|
|
|
error?: string
|
2024-07-21 21:30:03 +05:00
|
|
|
onChange: (name: string, value: string) => void
|
|
|
|
}
|
|
|
|
|
2024-07-24 15:13:28 +05:00
|
|
|
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'>
|
2024-10-21 16:39:56 +05:00
|
|
|
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'>
|
2024-08-16 14:32:49 +00:00
|
|
|
<div className='inputWrapperMain inputWrapperMainAlt'>
|
2024-07-24 15:13:28 +05:00
|
|
|
<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-12-12 14:20:57 +01:00
|
|
|
title='Remove'
|
2024-07-12 01:03:52 +05:00
|
|
|
>
|
2024-07-24 15:13:28 +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>
|
2024-08-16 15:30:23 +00:00
|
|
|
<div className='dropdown-menu dropdownMainMenu dropdownMainMenuAlt'>
|
2024-12-04 13:26:22 +01:00
|
|
|
<FixedSizeList
|
2024-07-24 15:13:28 +05:00
|
|
|
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>
|
|
|
|
)}
|
2024-12-04 13:26:22 +01:00
|
|
|
</FixedSizeList>
|
2024-07-24 15:13:28 +05:00
|
|
|
</div>
|
2024-07-12 01:03:52 +05:00
|
|
|
</div>
|
2024-10-21 16:39:56 +05:00
|
|
|
</div>
|
2024-10-19 07:47:42 +00:00
|
|
|
{error && <InputError message={error} />}
|
2024-10-21 16:39:56 +05:00
|
|
|
<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>
|
|
|
|
)
|
|
|
|
}
|