feat(mod): add permissions and details #202

Merged
enes merged 1 commits from extra/112-mods-fields into staging 2025-01-22 16:51:05 +00:00
5 changed files with 451 additions and 30 deletions

View File

@ -20,6 +20,9 @@ import {
DownloadUrl,
ModFormState,
ModPageLoaderResult,
ModPermissions,
MODPERMISSIONS_CONF,
MODPERMISSIONS_DESC,
SubmitModActionResult
} from '../types'
import {
@ -114,6 +117,13 @@ export const ModForm = () => {
[]
)
const handleRadioChange = useCallback((name: string, value: boolean) => {
setFormState((prevState) => ({
...prevState,
[name]: value
}))
}, [])
const addScreenshotUrl = useCallback(() => {
setFormState((prevState) => ({
...prevState,
@ -243,6 +253,17 @@ export const ModForm = () => {
[formState, isEditing, submit]
)
const extraBoxRef = useRef<HTMLDivElement>(null)
const handleExtraBoxButtonClick = () => {
if (extraBoxRef.current) {
if (extraBoxRef.current.style.display === '') {
extraBoxRef.current.style.display = 'none'
} else {
extraBoxRef.current.style.display = ''
}
}
}
return (
<form className='IBMSMSMBS_Write' onSubmit={handlePublish}>
<GameDropdown
@ -479,6 +500,131 @@ export const ModForm = () => {
<InputError message={formErrors?.downloadUrls[0]} />
)}
</div>
<div className='IBMSMSMBSSExtra'>
<button
className='btn btnMain IBMSMSMBSSExtraBtn'
type='button'
onClick={handleExtraBoxButtonClick}
>
Permissions &amp; Details
</button>
<div
className='IBMSMSMBSSExtraBox'
ref={extraBoxRef}
style={{
display: 'none'
}}
>
<p
className='labelDescriptionMain'
style={{ marginBottom: `10px`, textAlign: `center` }}
>
What permissions users have with your published mod/post
</p>
<div className='IBMSMSMBSSExtraBoxElementWrapper'>
{Object.keys(MODPERMISSIONS_CONF).map((k) => {
const permKey = k as keyof ModPermissions
const confKey = k as keyof typeof MODPERMISSIONS_CONF
const modPermission = MODPERMISSIONS_CONF[confKey]
const value = formState[permKey]
return (
<div className='IBMSMSMBSSExtraBoxElement' key={k}>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>{modPermission.header}</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<label
htmlFor={`${permKey}_true`}
className='IBMSMSMBSSExtraBoxElementColChoice'
>
<p>
{MODPERMISSIONS_DESC[`${permKey}_true`]}
<br />
</p>
<input
className='IBMSMSMBSSExtraBoxElementColChoiceRadio'
type='radio'
name={permKey}
id={`${permKey}_true`}
value={'true'}
checked={
typeof value !== 'undefined'
? value === true
: modPermission.default === true
}
onChange={(e) =>
handleRadioChange(
permKey,
e.currentTarget.value === 'true'
)
}
/>
<div className='IBMSMSMBSSExtraBoxElementColChoiceBox'></div>
</label>
<label
htmlFor={`${permKey}_false`}
className='IBMSMSMBSSExtraBoxElementColChoice'
>
<p>
{MODPERMISSIONS_DESC[`${permKey}_false`]}
<br />
</p>
<input
className='IBMSMSMBSSExtraBoxElementColChoiceRadio'
type='radio'
id={`${permKey}_false`}
value={'false'}
name={permKey}
checked={
typeof value !== 'undefined'
? value === false
: modPermission.default === false
}
onChange={(e) =>
handleRadioChange(
permKey,
e.currentTarget.value === 'true'
)
}
/>
<div className='IBMSMSMBSSExtraBoxElementColChoiceBox'></div>
</label>
</div>
</div>
)
})}
<div className='IBMSMSMBSSExtraBoxElement'>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>Publisher Notes</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<textarea
className='inputMain'
value={formState.publisherNotes || ''}
onChange={(e) =>
handleInputChange('publisherNotes', e.currentTarget.value)
}
/>
</div>
</div>
<div className='IBMSMSMBSSExtraBoxElement'>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>Extra Credits</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<textarea
className='inputMain'
value={formState.extraCredits || ''}
onChange={(e) =>
handleInputChange('extraCredits', e.currentTarget.value)
}
/>
</div>
</div>
</div>
</div>
</div>
<div className='IBMSMSMBS_WriteAction'>
<button
className='btn btnMain'

View File

@ -23,7 +23,15 @@ import '../../styles/styles.css'
import '../../styles/tabs.css'
import '../../styles/tags.css'
import '../../styles/write.css'
import { DownloadUrl, ModPageLoaderResult } from '../../types'
import {
DownloadUrl,
ModDetails,
ModFormState,
ModPageLoaderResult,
ModPermissions,
MODPERMISSIONS_CONF,
MODPERMISSIONS_DESC
} from '../../types'
import {
capitalizeEachWord,
checkUrlForFile,
@ -84,18 +92,7 @@ export const ModPage = () => {
<div className='IBMSMSplitMainBigSideSec'>
<Game />
{postWarning && <PostWarnings type={postWarning} />}
<Body
featuredImageUrl={mod.featuredImageUrl}
title={mod.title}
body={mod.body}
game={mod.game}
screenshotsUrls={mod.screenshotsUrls}
tags={mod.tags}
LTags={mod.LTags}
nsfw={mod.nsfw}
repost={mod.repost}
originalAuthor={mod.originalAuthor}
/>
<Body {...mod} />
<Interactions
addressable={mod}
commentCount={commentCount}
@ -390,18 +387,22 @@ const Game = () => {
)
}
type BodyProps = {
featuredImageUrl: string
title: string
body: string
game: string
screenshotsUrls: string[]
tags: string[]
LTags: string[]
nsfw: boolean
repost: boolean
originalAuthor?: string
}
type BodyProps = Pick<
ModDetails,
| 'featuredImageUrl'
| 'title'
| 'body'
| 'game'
| 'screenshotsUrls'
| 'tags'
| 'LTags'
| 'nsfw'
| 'repost'
| 'originalAuthor'
| keyof ModPermissions
| 'publisherNotes'
| 'extraCredits'
>
const Body = ({
featuredImageUrl,
@ -413,7 +414,15 @@ const Body = ({
LTags,
nsfw,
repost,
originalAuthor
originalAuthor,
otherAssets,
uploadPermission,
modPermission,
convPermission,
assetUsePermission,
assetUseComPermission,
publisherNotes,
extraCredits
}: BodyProps) => {
const COLLAPSED_MAX_SIZE = 250
const postBodyRef = useRef<HTMLDivElement>(null)
@ -485,6 +494,16 @@ const Body = ({
/>
))}
</div>
<ExtraDetails
otherAssets={otherAssets}
uploadPermission={uploadPermission}
modPermission={modPermission}
convPermission={convPermission}
assetUsePermission={assetUsePermission}
assetUseComPermission={assetUseComPermission}
publisherNotes={publisherNotes}
extraCredits={extraCredits}
/>
<div className='IBMSMSMBSSTags'>
{nsfw && (
<div className='IBMSMSMBSSTagsTag IBMSMSMBSSTagsTagNSFW'>
@ -833,3 +852,122 @@ const DisplayModAuthorBlogs = () => {
</div>
)
}
type ExtraDetailsProps = ModPermissions &
Pick<ModFormState, 'publisherNotes' | 'extraCredits'>
const ExtraDetails = ({
publisherNotes,
extraCredits,
...rest
}: ExtraDetailsProps) => {
const extraBoxRef = useRef<HTMLDivElement>(null)
if (
typeof publisherNotes === 'undefined' &&
typeof extraCredits === 'undefined' &&
Object.values(rest).every((v) => typeof v === 'undefined')
) {
return null
}
const handleClick = () => {
if (extraBoxRef.current) {
if (extraBoxRef.current.style.display === '') {
extraBoxRef.current.style.display = 'none'
} else {
extraBoxRef.current.style.display = ''
}
}
}
return (
<div className='IBMSMSMBSSExtra'>
<button
className='btn btnMain IBMSMSMBSSExtraBtn'
type='button'
onClick={handleClick}
>
Permissions &amp; Details
</button>
<div
className='IBMSMSMBSSExtraBox'
ref={extraBoxRef}
style={{
display: 'none'
}}
>
<div className='IBMSMSMBSSExtraBoxElementWrapper'>
{Object.keys(MODPERMISSIONS_CONF).map((k) => {
const permKey = k as keyof ModPermissions
const confKey = k as keyof typeof MODPERMISSIONS_CONF
const modPermission = MODPERMISSIONS_CONF[confKey]
const value = rest[permKey]
if (typeof value === 'undefined') return null
const text = MODPERMISSIONS_DESC[`${permKey}_${value}`]
return (
<div className='IBMSMSMBSSExtraBoxElement' key={k}>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>{modPermission.header}</p>
{value ? (
<div className='IBMSMSMBSSExtraBoxElementColMark IBMSMSMBSSExtraBoxElementColMarkGreen'>
<svg
className='IBMSMSMSSS_Author_Top_Icon'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM371.8 211.8C382.7 200.9 382.7 183.1 371.8 172.2C360.9 161.3 343.1 161.3 332.2 172.2L224 280.4L179.8 236.2C168.9 225.3 151.1 225.3 140.2 236.2C129.3 247.1 129.3 264.9 140.2 275.8L204.2 339.8C215.1 350.7 232.9 350.7 243.8 339.8L371.8 211.8z'></path>
</svg>
</div>
) : (
<div className='IBMSMSMBSSExtraBoxElementColMark IBMSMSMBSSExtraBoxElementColMarkRed'>
<svg
className='IBMSMSMSSS_Author_Top_Icon'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM175 208.1L222.1 255.1L175 303C165.7 312.4 165.7 327.6 175 336.1C184.4 346.3 199.6 346.3 208.1 336.1L255.1 289.9L303 336.1C312.4 346.3 327.6 346.3 336.1 336.1C346.3 327.6 346.3 312.4 336.1 303L289.9 255.1L336.1 208.1C346.3 199.6 346.3 184.4 336.1 175C327.6 165.7 312.4 165.7 303 175L255.1 222.1L208.1 175C199.6 165.7 184.4 165.7 175 175C165.7 184.4 165.7 199.6 175 208.1V208.1z'></path>
</svg>
</div>
)}
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<p>
{text}
<br />
</p>
</div>
</div>
)
})}
{typeof publisherNotes !== 'undefined' && publisherNotes !== '' && (
<div className='IBMSMSMBSSExtraBoxElement'>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>Publisher Notes</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<p>{publisherNotes}</p>
</div>
</div>
)}
{typeof extraCredits !== 'undefined' && extraCredits !== '' && (
<div className='IBMSMSMBSSExtraBoxElement'>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColStart'>
<p>Extra Credits</p>
</div>
<div className='IBMSMSMBSSExtraBoxElementCol IBMSMSMBSSExtraBoxElementColSecond'>
<p>{extraCredits}</p>
</div>
</div>
)}
</div>
</div>
</div>
)
}

View File

@ -8,6 +8,7 @@ import { store } from 'store'
import {
FormErrors,
ModFormState,
MODPERMISSIONS_CONF,
SubmitModActionResult,
TimeoutError
} from 'types'
@ -95,8 +96,41 @@ export const submitModRouteAction =
...formState.downloadUrls.map((downloadUrl) =>
JSON.stringify(downloadUrl)
)
]
],
[
'otherAssets',
formState.otherAssets?.toString() ??
MODPERMISSIONS_CONF.otherAssets.default.toString()
],
[
'uploadPermission',
formState.uploadPermission?.toString() ??
MODPERMISSIONS_CONF.uploadPermission.toString()
],
[
'modPermission',
formState.modPermission?.toString() ??
MODPERMISSIONS_CONF.modPermission.toString()
],
[
'convPermission',
formState.convPermission?.toString() ??
MODPERMISSIONS_CONF.convPermission.toString()
],
[
'assetUsePermission',
formState.assetUsePermission?.toString() ??
MODPERMISSIONS_CONF.assetUsePermission.toString()
],
[
'assetUseComPermission',
formState.assetUseComPermission?.toString() ??
MODPERMISSIONS_CONF.assetUseComPermission.toString()
],
['publisherNotes', formState.publisherNotes ?? ''],
['extraCredits', formState.extraCredits ?? '']
]
if (formState.repost && formState.originalAuthor) {
tags.push(['originalAuthor', formState.originalAuthor])
}

View File

@ -37,8 +37,29 @@ export interface ModFormState {
lTags: string[]
downloadUrls: DownloadUrl[]
published_at: number
// Permissions and details
otherAssets?: boolean
uploadPermission?: boolean
modPermission?: boolean
convPermission?: boolean
assetUsePermission?: boolean
assetUseComPermission?: boolean
publisherNotes?: string
extraCredits?: string
}
// Permissions
export type ModPermissions = Pick<
ModFormState,
| 'otherAssets'
| 'uploadPermission'
| 'modPermission'
| 'convPermission'
| 'assetUsePermission'
| 'assetUseComPermission'
>
export interface DownloadUrl {
url: string
title?: string
@ -95,3 +116,61 @@ export interface FormErrors {
author?: string
originalAuthor?: string
}
type ModPermissionsKeys = keyof ModPermissions
type ModPermissionsOpts = 'true' | 'false'
type ModPermissionsKeysOpts = `${ModPermissionsKeys}_${ModPermissionsOpts}`
type ModPermissionsDesc = {
[k in ModPermissionsKeysOpts]: string
}
type ModPermissionsConf = {
[k in ModPermissionsKeys]: {
header: string
default: boolean
}
}
export const MODPERMISSIONS_CONF: ModPermissionsConf = {
otherAssets: {
header: `Others' Assets`,
default: true
},
uploadPermission: {
header: `Redistribution Permission`,
default: true
},
modPermission: {
header: `Modification Permission`,
default: true
},
convPermission: {
header: `Conversion Permission`,
default: true
},
assetUsePermission: {
header: `Asset Use Permission`,
default: true
},
assetUseComPermission: {
header: `Asset Use Permission for Commercial Mods`,
default: false
}
}
export const MODPERMISSIONS_DESC: ModPermissionsDesc = {
otherAssets_true: `All assets in this file are either owned by the publisher or sourced from free-to-use modder's resources.`,
otherAssets_false: `Not all assets in this file are owned by the publisher or sourced from free-to-use modder's resources; some assets may be.`,
uploadPermission_true: `You are allowed to upload this file to other sites, but you must give credit to me as the creator (unless indicated otherwise).`,
uploadPermission_false: `You are not allowed to upload this file to other sites without explicit permission.`,
modPermission_true: `You may modify my files and release bug fixes or enhancements, provided that you credit me as the original creator.`,
modPermission_false: `You are not allowed to convert this file for use with other games without explicit permission.`,
convPermission_true: `You are permitted to convert this file for use with other games, as long as you credit me as the creator.`,
convPermission_false: `You are not permitted to convert this file for use with other games without explicit permission.`,
assetUsePermission_true: `You may use the assets in this file without needing permission, provided you give me credit.`,
assetUsePermission_false: `You must obtain explicit permission to use the assets in this file.`,
assetUseComPermission_true: `You are allowed to use assets from this file in mods or files that are sold for money on Steam Workshop or other platforms.`,
assetUseComPermission_false: `You are prohibited from using assets from this file in any mods or files that are sold for money on Steam Workshop or other platforms, unless given explicit permission.`
}

View File

@ -2,7 +2,7 @@ import _ from 'lodash'
import { NDKEvent } from '@nostr-dev-kit/ndk'
import { Event } from 'nostr-tools'
import { ModDetails, ModFormState } from '../types'
import { getTagValue, getTagValues } from './nostr'
import { getTagValue, getTagValues, getFirstTagValue as _getFirstTagValue } from './nostr'
/**
* Extracts and normalizes mod data from an event.
@ -26,6 +26,14 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
return tagValue ? parseInt(tagValue[0], 10) : defaultValue
}
// Optional mod values
const otherAssets = _getFirstTagValue(event, 'otherAssets')
const uploadPermission = _getFirstTagValue(event, 'uploadPermission')
const modPermission = _getFirstTagValue(event, 'modPermission')
const convPermission = _getFirstTagValue(event, 'convPermission')
const assetUsePermission = _getFirstTagValue(event, 'assetUsePermission')
const assetUseComPermission = _getFirstTagValue(event, 'assetUseComPermission')
return {
id: event.id,
dTag: getFirstTagValue('d'),
@ -52,7 +60,15 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
),
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
JSON.parse(item)
)
),
otherAssets: otherAssets ? otherAssets === 'true' : undefined,
uploadPermission: uploadPermission ? uploadPermission === 'true' : undefined,
modPermission: modPermission ? modPermission === 'true' : undefined,
convPermission: convPermission ? convPermission === 'true' : undefined,
assetUsePermission: assetUsePermission ? assetUsePermission === 'true' : undefined,
assetUseComPermission: assetUseComPermission ? assetUseComPermission === 'true' : undefined,
publisherNotes: _getFirstTagValue(event, 'publisherNotes'),
extraCredits: _getFirstTagValue(event, 'extraCredits')
}
}
@ -147,7 +163,15 @@ export const initializeFormState = (
customNote: '',
mediaUrl: ''
}
]
],
otherAssets: existingModData?.otherAssets ?? true,
uploadPermission: existingModData?.uploadPermission ?? true,
modPermission: existingModData?.modPermission ?? true,
convPermission:existingModData?.convPermission ?? true,
assetUsePermission:existingModData?.assetUsePermission ?? true,
assetUseComPermission:existingModData?.assetUseComPermission ?? false,
publisherNotes:existingModData?.publisherNotes || '',
extraCredits:existingModData?.extraCredits || ''
})
export const MOD_DRAFT_CACHE_KEY = 'draft-mod'