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' import '../styles/styles.css' 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 export const ModForm = () => { const userState = useAppSelector((state) => state.user) const [gameOptions, setGameOptions] = useState([]) const [formState, setFormState] = useState({ 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) => { 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 } return ( <>

We recommend to upload images to https://nostr.build/

{formState.screenshotsUrls.map((url, index) => ( ))}

You can upload your game mod to Github, as an example, and keep updating it there. Also, it's advisable that you hash your package as well with your nostr public key.

{formState.downloadUrls.map((download, index) => ( ))}
) } type DownloadUrlFieldsProps = { index: number download: DownloadUrl onUrlChange: (index: number, field: keyof DownloadUrl, value: string) => void onRemove: (index: number) => void } const DownloadUrlFields = React.memo( ({ index, download, 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 onChange: (name: string, value: string) => void } const GameDropdown = ({ options, selected, 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? Send us a DM mentioning it so we can 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}
)}
) }