import _ from 'lodash' import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useActionData, useLoaderData, useNavigation, useSubmit } from 'react-router-dom' import { FixedSizeList } from 'react-window' import { useGames } from '../hooks' import '../styles/styles.css' import { DownloadUrl, FormErrors, ModFormState, ModPageLoaderResult } from '../types' import { initializeFormState } from '../utils' import { CheckboxField, InputField, InputFieldWithImageUpload } from './Inputs' import { OriginalAuthor } from './OriginalAuthor' import { CategoryAutocomplete } from './CategoryAutocomplete' import { AlertPopup } from './AlertPopup' import { Editor, EditorRef } from './Markdown/Editor' import { MEDIA_OPTIONS } from 'controllers' import { InputError } from './Inputs/Error' import { ImageUpload } from './Inputs/ImageUpload' interface GameOption { value: string label: string } export const ModForm = () => { const data = useLoaderData() as ModPageLoaderResult const mod = data?.mod const formErrors = useActionData() as FormErrors const navigation = useNavigation() const submit = useSubmit() const games = useGames() const [gameOptions, setGameOptions] = useState([]) const [formState, setFormState] = useState( initializeFormState(mod) ) console.log(`[debug] screenshots`, formState.screenshotsUrls) const editorRef = useRef(null) useEffect(() => { const options = games.map((game) => ({ label: game['Game Name'], value: game['Game Name'] })) setGameOptions(options) }, [games]) const handleInputChange = useCallback((name: string, value: string) => { setFormState((prevState) => ({ ...prevState, [name]: value })) }, []) const handleCheckboxChange = useCallback( (e: React.ChangeEvent) => { 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 [showConfirmPopup, setShowConfirmPopup] = useState(false) const handleReset = () => { setShowConfirmPopup(true) } const handleResetConfirm = (confirm: boolean) => { setShowConfirmPopup(false) // Cancel if not confirmed if (!confirm) return // Editing if (mod) { const initial = initializeFormState(mod) // Reset editor editorRef.current?.setMarkdown(initial.body) // Reset fields to the original existing data setFormState(initial) return } // New - set form state to the initial (clear form state) setFormState(initializeFormState()) } const handlePublish = () => { submit(JSON.stringify(formState), { method: mod ? 'put' : 'post', encType: 'application/json' }) } return (
{ e.preventDefault() handlePublish() }} >
{ handleInputChange('body', md) }} />
{typeof formErrors?.body !== 'undefined' && ( )}
{formState.repost && ( <> Created by:{' '} {} } type='text' placeholder="Original author's name, npub or nprofile" name='originalAuthor' value={formState.originalAuthor || ''} error={formErrors?.originalAuthor} onChange={handleInputChange} /> )}

We recommend to upload images to {MEDIA_OPTIONS[0].host}

{ // values.forEach((screenshotUrl) => addScreenshotUrl()) setFormState((prevState) => ({ ...prevState, screenshotsUrls: Array.from( new Set([ ...prevState.screenshotsUrls.filter((url) => url), ...values ]) ) })) }} /> {formState.screenshotsUrls.map((url, index) => ( {formErrors?.screenshotsUrls && formErrors?.screenshotsUrls[index] && ( )} ))} {formState.screenshotsUrls.length === 0 && formErrors?.screenshotsUrls && formErrors?.screenshotsUrls[0] && ( )}

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.

{formState.downloadUrls.map((download, index) => ( {formErrors?.downloadUrls && formErrors?.downloadUrls[index] && ( )} ))} {formState.downloadUrls.length === 0 && formErrors?.downloadUrls && formErrors?.downloadUrls[0] && ( )}
{showConfirmPopup && ( setShowConfirmPopup(false)} header={'Are you sure?'} label={ mod ? `Are you sure you want to clear all changes?` : `Are you sure you want to clear all field data?` } /> )} ) } type DownloadUrlFieldsProps = { index: number url: string hash: string signatureKey: string malwareScanLink: string modVersion: string customNote: string onUrlChange: (index: number, field: keyof DownloadUrl, value: string) => void onRemove: (index: number) => void } const DownloadUrlFields = React.memo( ({ index, url, hash, signatureKey, malwareScanLink, modVersion, customNote, onUrlChange, onRemove }: DownloadUrlFieldsProps) => { const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target onUrlChange(index, name as keyof DownloadUrl, value) } return (
) } ) 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) => { onUrlChange(index, e.target.value) } return (
) } ) type GameDropdownProps = { options: GameOption[] selected: string error?: string onChange: (name: string, value: string) => void } const GameDropdown = ({ options, selected, error, onChange }: GameDropdownProps) => { const [searchTerm, setSearchTerm] = useState('') const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') const inputRef = useRef(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 (

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.

handleSearchChange(e.target.value)} onBlur={() => { if (!isOptionClicked.current) { setSearchTerm('') } isOptionClicked.current = false }} />
{({ index, style }) => (
(isOptionClicked.current = true)} onClick={() => { onChange('game', filteredOptions[index].value) setSearchTerm('') if (inputRef.current) { inputRef.current.blur() } }} > {filteredOptions[index].label}
)}
{error && }

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.

) }