chore: add state to modForm

This commit is contained in:
daniyal 2024-07-21 21:30:03 +05:00
parent fd93a6cf5d
commit deaf0ddac9
10 changed files with 204837 additions and 236 deletions

964
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,16 +12,29 @@
"dependencies": { "dependencies": {
"@nostr-dev-kit/ndk": "2.8.2", "@nostr-dev-kit/ndk": "2.8.2",
"@reduxjs/toolkit": "2.2.6", "@reduxjs/toolkit": "2.2.6",
"lodash": "4.17.21",
"nostr-login": "1.5.2", "nostr-login": "1.5.2",
"nostr-tools": "2.7.1", "nostr-tools": "2.7.1",
"papaparse": "5.4.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-quill": "2.0.0",
"react-redux": "9.1.2", "react-redux": "9.1.2",
"react-router-dom": "^6.24.1" "react-router-dom": "^6.24.1",
"react-select": "5.8.0",
"react-toastify": "10.0.5",
"react-virtualized-auto-sizer": "1.0.24",
"react-window": "1.8.10",
"uuid": "10.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "4.17.7",
"@types/papaparse": "5.3.14",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"@types/react-select": "5.0.1",
"@types/react-window": "1.8.8",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1", "@typescript-eslint/parser": "^7.13.1",
"@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react": "^4.3.1",

203171
public/assets/games.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +1,121 @@
import '../styles/styles.css' import '../styles/styles.css'
import ReactQuill from 'react-quill'
import '../styles/customQuillStyles.css'
import React from 'react'
const editorFormats = [
'header',
'font',
'size',
'bold',
'italic',
'underline',
'strike',
'blockquote',
'list',
'bullet',
'indent',
'link'
]
const editorModules = {
toolbar: [
[{ header: '1' }, { header: '2' }, { font: [] }],
[{ size: [] }],
['bold', 'italic', 'underline', 'strike', 'blockquote'],
[
{ list: 'ordered' },
{ list: 'bullet' },
{ indent: '-1' },
{ indent: '+1' }
],
['link']
]
}
interface InputFieldProps { interface InputFieldProps {
label: string label: string
description?: string description?: string
type?: 'text' | 'textarea' type?: 'text' | 'textarea' | 'richtext'
placeholder: string placeholder: string
name: string name: string
inputMode?: 'url' inputMode?: 'url'
value: string
onChange: (name: string, value: string) => void
} }
export const InputField = ({ export const InputField = React.memo(
label, ({
description, label,
type = 'text', description,
placeholder, type = 'text',
name, placeholder,
inputMode name,
}: InputFieldProps) => ( inputMode,
<div className='inputLabelWrapperMain'> value,
<label className='form-label labelMain'>{label}</label> onChange
{description && <p className='labelDescriptionMain'>{description}</p>} }: InputFieldProps) => {
{type === 'textarea' ? ( const handleChange = (
<textarea e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
className='inputMain' ) => {
placeholder={placeholder} onChange(name, e.target.value)
name={name} }
></textarea>
) : ( return (
<input <div className='inputLabelWrapperMain'>
type={type} <label className='form-label labelMain'>{label}</label>
className='inputMain' {description && <p className='labelDescriptionMain'>{description}</p>}
placeholder={placeholder} {type === 'textarea' ? (
name={name} <textarea
inputMode={inputMode} className='inputMain'
/> placeholder={placeholder}
)} name={name}
</div> value={value}
onChange={handleChange}
></textarea>
) : type === 'richtext' ? (
<ReactQuill
className='inputMain'
formats={editorFormats}
modules={editorModules}
placeholder={placeholder}
value={value}
onChange={(content) => onChange(name, content)}
/>
) : (
<input
type={type}
className='inputMain'
placeholder={placeholder}
name={name}
inputMode={inputMode}
value={value}
onChange={handleChange}
/>
)}
</div>
)
}
) )
interface CheckboxFieldProps { interface CheckboxFieldProps {
label: string label: string
name: string name: string
isChecked: boolean
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
} }
export const CheckboxField = ({ label, name }: CheckboxFieldProps) => ( export const CheckboxField = React.memo(
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'> ({ label, name, isChecked, handleChange }: CheckboxFieldProps) => (
<label className='form-label labelMain'>{label}</label> <div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<input type='checkbox' className='CheckboxMain' name={name} /> <label className='form-label labelMain'>{label}</label>
</div>
)
interface ImageUploadFieldProps {
label: string
description: string
}
export const ImageUploadField = ({
label,
description
}: ImageUploadFieldProps) => (
<div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>{label}</label>
{description && <p className='labelDescriptionMain'>{description}</p>}
<div className='inputWrapperMain'>
<input <input
type='text' type='checkbox'
className='inputMain' className='CheckboxMain'
inputMode='url' name={name}
placeholder='We recommend to upload images to https://nostr.build/' checked={isChecked}
onChange={handleChange}
/> />
<button className='btn btnMain btnMainRemove' type='button'>
<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>
</div> </div>
</div> )
) )

View File

@ -1,26 +1,308 @@
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 '../styles/styles.css'
import { CheckboxField, ImageUploadField, InputField } from './Inputs' 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 = () => { export const ModForm = () => {
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
}
return ( return (
<> <>
<InputField <GameDropdown
label='Game' options={gameOptions}
description="Can't find the game you're looking for? Send us a DM mentioning it so we can add it." selected={formState.game}
placeholder='The mod is for a game called...' onChange={handleInputChange}
name='game'
/> />
<InputField <InputField
label='Title' label='Title'
placeholder='Return the banana mod' placeholder='Return the banana mod'
name='title' name='title'
value={formState.title}
onChange={handleInputChange}
/> />
<InputField <InputField
label='Body' label='Body'
type='textarea' type='richtext'
placeholder="Here's what this mod is all about" placeholder="Here's what this mod is all about"
name='body' name='body'
value={formState.body}
onChange={handleInputChange}
/> />
<InputField <InputField
label='Featured Image URL' label='Featured Image URL'
description='We recommend to upload images to https://nostr.build/' description='We recommend to upload images to https://nostr.build/'
@ -28,29 +310,71 @@ export const ModForm = () => {
inputMode='url' inputMode='url'
placeholder='Image URL' placeholder='Image URL'
name='featuredImageUrl' name='featuredImageUrl'
value={formState.featuredImageUrl}
onChange={handleInputChange}
/> />
<InputField <InputField
label='Summary' label='Summary'
type='textarea' type='textarea'
placeholder='This is a quick description of my mod' placeholder='This is a quick description of my mod'
name='summary' name='summary'
value={formState.summary}
onChange={handleInputChange}
/> />
<CheckboxField label='This mod not safe for work (NSFW)' name='nsfw' /> <CheckboxField
<ImageUploadField label='This mod not safe for work (NSFW)'
label='Screenshots URLs' name='nsfw'
description='We recommend to upload images to https://nostr.build/' isChecked={formState.nsfw}
handleChange={handleCheckboxChange}
/> />
<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>
<InputField <InputField
label='Tags' label='Tags'
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)' description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
placeholder='Tags' placeholder='Tags'
name='tags' name='tags'
value={formState.tags}
onChange={handleInputChange}
/> />
<div className='inputLabelWrapperMain'> <div className='inputLabelWrapperMain'>
<div className='labelWrapperMain'> <div className='labelWrapperMain'>
<label className='form-label labelMain'>Download URLs</label> <label className='form-label labelMain'>Download URLs</label>
<button className='btn btnMain btnMainAdd' type='button'> <button
className='btn btnMain btnMainAdd'
type='button'
onClick={addDownloadUrl}
>
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='-32 0 512 512' viewBox='-32 0 512 512'
@ -64,29 +388,213 @@ export const ModForm = () => {
</div> </div>
<p className='labelDescriptionMain'> <p className='labelDescriptionMain'>
You can upload your game mod to Github, as an example, and keep You can upload your game mod to Github, as an example, and keep
updating it there. Also, its advisable that you hash your package as updating it there. Also, it's advisable that you hash your package as
well with your nostr public key. well with your nostr public key.
</p> </p>
<DownloadUrlFields /> {formState.downloadUrls.map((download, index) => (
<DownloadUrlFields /> <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>
</div> </div>
</> </>
) )
} }
type DownloadUrlFieldsProps = {
index: number
download: DownloadUrl
onUrlChange: (index: number, field: keyof DownloadUrl, value: string) => void
onRemove: (index: number) => void
}
const DownloadUrlFields = () => { const DownloadUrlFields = React.memo(
return ( ({ index, download, onUrlChange, onRemove }: DownloadUrlFieldsProps) => {
<div className='inputWrapperMainWrapper'> 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)}
>
<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>
</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='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>
</div>
</div>
)
}
)
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 (
<div className='inputWrapperMain'> <div className='inputWrapperMain'>
<input <input
type='text' type='text'
className='inputMain' className='inputMain'
inputMode='url' inputMode='url'
placeholder='https://...' placeholder='We recommend to upload images to https://nostr.build/'
value='https://github.com/' value={url}
onChange={handleChange}
/> />
<button className='btn btnMain btnMainRemove' type='button'> <button
className='btn btnMain btnMainRemove'
type='button'
onClick={() => onRemove(index)}
>
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='-32 0 512 512' viewBox='-32 0 512 512'
@ -94,110 +602,91 @@ const DownloadUrlFields = () => {
height='1em' height='1em'
fill='currentColor' 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> <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>
</svg> </svg>
</button> </button>
</div> </div>
<div className='inputWrapperMain'> )
<div className='inputWrapperMainBox'> }
<svg )
xmlns='http://www.w3.org/2000/svg'
viewBox='-96 0 512 512' type GameDropdownProps = {
width='1em' options: GameOption[]
height='1em' selected: string
fill='currentColor' onChange: (name: string, value: string) => void
> }
<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> const GameDropdown = ({ options, selected, onChange }: GameDropdownProps) => {
</div> 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'>
<input <input
ref={inputRef}
type='text' type='text'
className='inputMain' className='inputMain dropdown-toggle'
inputMode='url' placeholder='This mod is for a game called...'
placeholder='SHA-256 Hash' aria-expanded='false'
data-bs-toggle='dropdown'
value={searchTerm || selected}
onChange={(e) => handleSearchChange(e.target.value)}
onBlur={() => {
if (!isOptionClicked.current) {
setSearchTerm('')
}
isOptionClicked.current = false
}}
/> />
<div className='inputWrapperMainBox'></div> <div className='dropdown-menu dropdownMainMenu'>
</div> <List
<div className='inputWrapperMain'> height={500}
<div className='inputWrapperMainBox'> width={'100%'}
<svg itemCount={filteredOptions.length}
xmlns='http://www.w3.org/2000/svg' itemSize={35}
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> {({ index, style }) => (
</svg> <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> </div>
<input
type='text'
className='inputMain'
inputMode='url'
placeholder='Signature public key'
value='npub18n4ysp43ux5c98fs6h9c57qpr4p8r3j8f6e32v0vj8egzy878aqqyzzk9r'
/>
<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'
inputMode='url'
placeholder='Malware scan link'
/>
<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'
inputMode='url'
placeholder='Mod version (1.0)'
/>
<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'
inputMode='url'
placeholder='Custome note/message'
/>
<div className='inputWrapperMainBox'></div>
</div> </div>
</div> </div>
) )

View File

@ -2,6 +2,8 @@ import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import App from './App.tsx' import App from './App.tsx'
import './index.css' import './index.css'
import { store } from './store/index.ts' import { store } from './store/index.ts'
@ -11,6 +13,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}> <Provider store={store}>
<BrowserRouter> <BrowserRouter>
<App /> <App />
<ToastContainer />
</BrowserRouter> </BrowserRouter>
</Provider> </Provider>
</React.StrictMode> </React.StrictMode>

View File

@ -16,11 +16,6 @@ export const SubmitModPage = () => {
</div> </div>
<div className='IBMSMSMBS_Write'> <div className='IBMSMSMBS_Write'>
<ModForm /> <ModForm />
<div className='IBMSMSMBS_WriteAction'>
<button className='btn btnMain' type='button'>
Publish
</button>
</div>
</div> </div>
</div> </div>
<ProfileSection /> <ProfileSection />

View File

@ -7,7 +7,7 @@ export interface IUserState {
} }
const initialState: IUserState = { const initialState: IUserState = {
isAuth: localStorage.getItem('login') ? true : false, isAuth: false,
user: {} user: {}
} }

View File

@ -0,0 +1,13 @@
.quill .ql-toolbar.ql-snow {
border: none;
border-bottom: inherit;
}
.quill .ql-container.ql-snow {
border: none;
}
.quill .ql-editor.ql-blank::before {
color: #757575;
font-size: medium;
}

View File

@ -1,5 +1,16 @@
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
/**
* Get the current time in seconds since the Unix epoch (January 1, 1970).
*
* This function retrieves the current time in milliseconds since the Unix epoch
* using `Date.now()` and then converts it to seconds by dividing by 1000.
* Finally, it rounds the result to the nearest whole number using `Math.round()`.
*
* @returns {number} The current time in seconds since the Unix epoch.
*/
export const now = () => Math.round(Date.now() / 1000)
/** /**
* Converts a hexadecimal public key to an npub format. * Converts a hexadecimal public key to an npub format.
* If the input is already in npub format, it returns the input unchanged. * If the input is already in npub format, it returns the input unchanged.