feat: initial categories
This commit is contained in:
parent
8c6046ac6d
commit
3b2dce54c5
24
src/assets/categories/categories.json
Normal file
24
src/assets/categories/categories.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "audio",
|
||||||
|
"sub": [
|
||||||
|
{ "name": "music", "sub": ["background", "ambient"] },
|
||||||
|
{ "name": "sound effects", "sub": ["footsteps", "weapons"] },
|
||||||
|
"voice"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "graphical",
|
||||||
|
"sub": [
|
||||||
|
{
|
||||||
|
"name": "textures",
|
||||||
|
"sub": ["highres textures", "lowres textures"]
|
||||||
|
},
|
||||||
|
"models",
|
||||||
|
"shaders"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ "name": "user interface", "sub": ["hud", "menus", "icons"] },
|
||||||
|
{ "name": "gameplay", "sub": ["mechanics", "balance", "ai"] },
|
||||||
|
"bugfixes"
|
||||||
|
]
|
@ -16,8 +16,10 @@ import { T_TAG_VALUE } from '../constants'
|
|||||||
import { useAppSelector, useGames, useNDKContext } from '../hooks'
|
import { useAppSelector, useGames, useNDKContext } from '../hooks'
|
||||||
import { appRoutes, getModPageRoute } from '../routes'
|
import { appRoutes, getModPageRoute } from '../routes'
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import { DownloadUrl, ModDetails, ModFormState } from '../types'
|
import { Categories, DownloadUrl, ModDetails, ModFormState } from '../types'
|
||||||
import {
|
import {
|
||||||
|
capitalizeEachWord,
|
||||||
|
getCategories,
|
||||||
initializeFormState,
|
initializeFormState,
|
||||||
isReachable,
|
isReachable,
|
||||||
isValidImageUrl,
|
isValidImageUrl,
|
||||||
@ -231,6 +233,21 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
|
|||||||
tags.push(['originalAuthor', formState.originalAuthor])
|
tags.push(['originalAuthor', formState.originalAuthor])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepend com.degmods to avoid leaking categories to 3rd party client's search
|
||||||
|
// Add heirarchical namespaces labels
|
||||||
|
if (formState.LTags.length > 0) {
|
||||||
|
for (let i = 0; i < formState.LTags.length; i++) {
|
||||||
|
tags.push(['L', `com.degmods:${formState.LTags[i]}`])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add category labels
|
||||||
|
if (formState.lTags.length > 0) {
|
||||||
|
for (let i = 0; i < formState.lTags.length; i++) {
|
||||||
|
tags.push(['l', `com.degmods:${formState.lTags[i]}`])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const unsignedEvent: UnsignedEvent = {
|
const unsignedEvent: UnsignedEvent = {
|
||||||
kind: kinds.ClassifiedListing,
|
kind: kinds.ClassifiedListing,
|
||||||
created_at: currentTimeStamp,
|
created_at: currentTimeStamp,
|
||||||
@ -489,6 +506,11 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
|
|||||||
error={formErrors.tags}
|
error={formErrors.tags}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
<CategoryAutocomplete
|
||||||
|
lTags={formState.lTags}
|
||||||
|
LTags={formState.LTags}
|
||||||
|
setFormState={setFormState}
|
||||||
|
/>
|
||||||
<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>
|
||||||
@ -532,7 +554,6 @@ export const ModForm = ({ existingModData }: ModFormProps) => {
|
|||||||
)}
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{formState.downloadUrls.length === 0 &&
|
{formState.downloadUrls.length === 0 &&
|
||||||
formErrors.downloadUrls &&
|
formErrors.downloadUrls &&
|
||||||
formErrors.downloadUrls[0] && (
|
formErrors.downloadUrls[0] && (
|
||||||
@ -878,3 +899,227 @@ const GameDropdown = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CategoryAutocompleteProps {
|
||||||
|
lTags: string[]
|
||||||
|
LTags: string[]
|
||||||
|
setFormState: (value: React.SetStateAction<ModFormState>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CategoryAutocomplete = ({
|
||||||
|
lTags,
|
||||||
|
LTags,
|
||||||
|
setFormState
|
||||||
|
}: CategoryAutocompleteProps) => {
|
||||||
|
const flattenedCategories = getCategories()
|
||||||
|
const initialCategories = lTags.map((name) => {
|
||||||
|
const existingCategory = flattenedCategories.find(
|
||||||
|
(cat) => cat.name === name
|
||||||
|
)
|
||||||
|
if (existingCategory) {
|
||||||
|
return existingCategory
|
||||||
|
} else {
|
||||||
|
return { name, hierarchy: name, l: [name] }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const [selectedCategories, setSelectedCategories] =
|
||||||
|
useState<Categories[]>(initialCategories)
|
||||||
|
const [inputValue, setInputValue] = useState<string>('')
|
||||||
|
const [filteredOptions, setFilteredOptions] = useState<Categories[]>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newFilteredOptions = flattenedCategories.filter((option) =>
|
||||||
|
option.hierarchy.toLowerCase().includes(inputValue.toLowerCase())
|
||||||
|
)
|
||||||
|
setFilteredOptions(newFilteredOptions)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [inputValue])
|
||||||
|
|
||||||
|
const getSelectedCategories = (cats: Categories[]) => {
|
||||||
|
const uniqueValues = new Set(
|
||||||
|
cats.reduce<string[]>((prev, cat) => [...prev, ...cat.l], [])
|
||||||
|
)
|
||||||
|
const concatenatedValue = Array.from(uniqueValues)
|
||||||
|
return concatenatedValue
|
||||||
|
}
|
||||||
|
const getSelectedHeirarchy = (cats: Categories[]) => {
|
||||||
|
const heirarchies = cats.reduce<string[]>(
|
||||||
|
(prev, cat) => [...prev, cat.hierarchy.replace(/ > /g, ':')],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const concatenatedValue = Array.from(heirarchies)
|
||||||
|
return concatenatedValue
|
||||||
|
}
|
||||||
|
const handleReset = () => {
|
||||||
|
setSelectedCategories([])
|
||||||
|
setInputValue('')
|
||||||
|
}
|
||||||
|
const handleRemove = (option: Categories) => {
|
||||||
|
setSelectedCategories(
|
||||||
|
selectedCategories.filter((cat) => cat.hierarchy !== option.hierarchy)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const handleSelect = (option: Categories) => {
|
||||||
|
if (!selectedCategories.some((cat) => cat.hierarchy === option.hierarchy)) {
|
||||||
|
setSelectedCategories([...selectedCategories, option])
|
||||||
|
}
|
||||||
|
setInputValue('')
|
||||||
|
}
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setInputValue(e.target.value)
|
||||||
|
}
|
||||||
|
const handleAddNew = () => {
|
||||||
|
if (inputValue) {
|
||||||
|
const newOption: Categories = {
|
||||||
|
name: inputValue,
|
||||||
|
hierarchy: inputValue,
|
||||||
|
l: [inputValue]
|
||||||
|
}
|
||||||
|
setSelectedCategories([...selectedCategories, newOption])
|
||||||
|
setInputValue('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
['lTags']: getSelectedCategories(selectedCategories),
|
||||||
|
['LTags']: getSelectedHeirarchy(selectedCategories)
|
||||||
|
}))
|
||||||
|
}, [selectedCategories, setFormState])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='inputLabelWrapperMain'>
|
||||||
|
<label className='form-label labelMain'>Categories</label>
|
||||||
|
<p className='labelDescriptionMain'>You can select multiple categories</p>
|
||||||
|
<div className='dropdown dropdownMain'>
|
||||||
|
<div className='inputWrapperMain inputWrapperMainAlt'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='inputMain inputMainWithBtn dropdown-toggle'
|
||||||
|
placeholder='Select some categories...'
|
||||||
|
aria-expanded='false'
|
||||||
|
data-bs-toggle='dropdown'
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='btn btnMain btnMainInsideField btnMainRemove'
|
||||||
|
type='button'
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className='dropdown-menu dropdownMainMenu dropdownMainMenuAlt'>
|
||||||
|
{filteredOptions && filteredOptions.length > 0 ? (
|
||||||
|
<List
|
||||||
|
height={500}
|
||||||
|
width={'100%'}
|
||||||
|
itemCount={filteredOptions.length}
|
||||||
|
itemSize={35}
|
||||||
|
>
|
||||||
|
{({ index, style }) => (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className='dropdown-item dropdownMainMenuItem'
|
||||||
|
onClick={() => handleSelect(filteredOptions[index])}
|
||||||
|
>
|
||||||
|
{capitalizeEachWord(filteredOptions[index].hierarchy)}
|
||||||
|
{selectedCategories.some(
|
||||||
|
(cat) =>
|
||||||
|
cat.hierarchy === filteredOptions[index].hierarchy
|
||||||
|
) && (
|
||||||
|
<button
|
||||||
|
className='btn btnMain btnMainInsideField btnMainRemove'
|
||||||
|
onClick={() => handleRemove(filteredOptions[index])}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-32 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
) : (
|
||||||
|
<List height={500} width={'100%'} itemCount={1} itemSize={35}>
|
||||||
|
{({ index, style }) => (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className='dropdown-item dropdownMainMenuItem'
|
||||||
|
onClick={handleAddNew}
|
||||||
|
>
|
||||||
|
{inputValue &&
|
||||||
|
!filteredOptions?.find(
|
||||||
|
(option) =>
|
||||||
|
option.hierarchy.toLowerCase() ===
|
||||||
|
inputValue.toLowerCase()
|
||||||
|
) ? (
|
||||||
|
<>
|
||||||
|
Add "{inputValue}"
|
||||||
|
<button className='btn btnMain btnMainInsideField btnMainRemove'>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>No matches</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{LTags.length > 0 && (
|
||||||
|
<div className='IBMSMSMBSSCategories'>
|
||||||
|
{LTags.map((hierarchy) => {
|
||||||
|
const heirarchicalCategories = hierarchy.split(`:`)
|
||||||
|
const categories = heirarchicalCategories
|
||||||
|
.map<React.ReactNode>((c: string) => (
|
||||||
|
<a className='IBMSMSMBSSCategoriesBoxItem'>
|
||||||
|
<p>{capitalizeEachWord(c)}</p>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
.reduce((prev, curr) => [
|
||||||
|
prev,
|
||||||
|
<div className='IBMSMSMBSSCategoriesBoxSeparator'>
|
||||||
|
<p>></p>
|
||||||
|
</div>,
|
||||||
|
curr
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={hierarchy} className='IBMSMSMBSSCategoriesBox'>
|
||||||
|
{categories}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -105,6 +105,8 @@ export const ModPage = () => {
|
|||||||
body={mod.body}
|
body={mod.body}
|
||||||
screenshotsUrls={mod.screenshotsUrls}
|
screenshotsUrls={mod.screenshotsUrls}
|
||||||
tags={mod.tags}
|
tags={mod.tags}
|
||||||
|
LTags={mod.LTags}
|
||||||
|
lTags={mod.lTags}
|
||||||
nsfw={mod.nsfw}
|
nsfw={mod.nsfw}
|
||||||
repost={mod.repost}
|
repost={mod.repost}
|
||||||
originalAuthor={mod.originalAuthor}
|
originalAuthor={mod.originalAuthor}
|
||||||
@ -426,6 +428,8 @@ type BodyProps = {
|
|||||||
body: string
|
body: string
|
||||||
screenshotsUrls: string[]
|
screenshotsUrls: string[]
|
||||||
tags: string[]
|
tags: string[]
|
||||||
|
LTags: string[]
|
||||||
|
lTags: string[]
|
||||||
nsfw: boolean
|
nsfw: boolean
|
||||||
repost: boolean
|
repost: boolean
|
||||||
originalAuthor?: string
|
originalAuthor?: string
|
||||||
@ -437,6 +441,8 @@ const Body = ({
|
|||||||
body,
|
body,
|
||||||
screenshotsUrls,
|
screenshotsUrls,
|
||||||
tags,
|
tags,
|
||||||
|
LTags,
|
||||||
|
lTags,
|
||||||
nsfw,
|
nsfw,
|
||||||
repost,
|
repost,
|
||||||
originalAuthor
|
originalAuthor
|
||||||
@ -532,6 +538,33 @@ const Body = ({
|
|||||||
{tag}
|
{tag}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{LTags.length > 0 && (
|
||||||
|
<div className='IBMSMSMBSSCategories'>
|
||||||
|
{LTags.map((hierarchy) => {
|
||||||
|
const heirarchicalCategories = hierarchy.split(`:`)
|
||||||
|
const categories = heirarchicalCategories
|
||||||
|
.map<React.ReactNode>((c: string) => (
|
||||||
|
<a className='IBMSMSMBSSCategoriesBoxItem'>
|
||||||
|
<p>{c}</p>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
.reduce((prev, curr) => [
|
||||||
|
prev,
|
||||||
|
<div className='IBMSMSMBSSCategoriesBoxSeparator'>
|
||||||
|
<p>></p>
|
||||||
|
</div>,
|
||||||
|
curr
|
||||||
|
])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={hierarchy} className='IBMSMSMBSSCategoriesBox'>
|
||||||
|
{categories}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,7 +43,8 @@ import { useCuratedSet } from 'hooks/useCuratedSet'
|
|||||||
enum SearchKindEnum {
|
enum SearchKindEnum {
|
||||||
Mods = 'Mods',
|
Mods = 'Mods',
|
||||||
Games = 'Games',
|
Games = 'Games',
|
||||||
Users = 'Users'
|
Users = 'Users',
|
||||||
|
Categories = 'Categories'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchPage = () => {
|
export const SearchPage = () => {
|
||||||
@ -132,6 +133,10 @@ export const SearchPage = () => {
|
|||||||
{searchKind === SearchKindEnum.Games && (
|
{searchKind === SearchKindEnum.Games && (
|
||||||
<GamesResult searchTerm={searchTerm} />
|
<GamesResult searchTerm={searchTerm} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{searchKind === SearchKindEnum.Categories && (
|
||||||
|
<CategoriesResult searchTerm={searchTerm} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -538,3 +543,44 @@ function dedup(event1: NDKEvent, event2: NDKEvent) {
|
|||||||
|
|
||||||
return event2
|
return event2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CategoriesResultProps {
|
||||||
|
searchTerm: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CategoriesResult = ({ searchTerm }: CategoriesResultProps) => {
|
||||||
|
const { ndk } = useNDKContext()
|
||||||
|
const [mods, setMods] = useState<ModDetails[]>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const call = async () => {
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
kinds: [NDKKind.Classified],
|
||||||
|
'#l': [`com.degmods:${searchTerm}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndkEventSet = await ndk.fetchEvents(filter)
|
||||||
|
const events = Array.from(ndkEventSet)
|
||||||
|
const mods: ModDetails[] = []
|
||||||
|
events.map((e) => {
|
||||||
|
if (isModDataComplete(e)) {
|
||||||
|
const mod = extractModData(e)
|
||||||
|
mods.push(mod)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setMods(mods)
|
||||||
|
}
|
||||||
|
|
||||||
|
call()
|
||||||
|
}, [ndk, searchTerm])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
|
<div className='IBMSMList'>
|
||||||
|
{mods?.map((mod) => (
|
||||||
|
<ModCard key={mod.id} {...mod} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
12
src/types/category.ts
Normal file
12
src/types/category.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface Category {
|
||||||
|
name: string
|
||||||
|
sub?: (Category | string)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoriesData = Category[]
|
||||||
|
|
||||||
|
export interface Categories {
|
||||||
|
name: string
|
||||||
|
hierarchy: string
|
||||||
|
l: string[]
|
||||||
|
}
|
@ -4,3 +4,4 @@ export * from './nostr'
|
|||||||
export * from './user'
|
export * from './user'
|
||||||
export * from './zap'
|
export * from './zap'
|
||||||
export * from './blog'
|
export * from './blog'
|
||||||
|
export * from './category'
|
||||||
|
@ -31,6 +31,10 @@ export interface ModFormState {
|
|||||||
originalAuthor?: string
|
originalAuthor?: string
|
||||||
screenshotsUrls: string[]
|
screenshotsUrls: string[]
|
||||||
tags: string
|
tags: string
|
||||||
|
/** Hierarchical labels */
|
||||||
|
LTags: string[]
|
||||||
|
/** Category labels for category search */
|
||||||
|
lTags: string[]
|
||||||
downloadUrls: DownloadUrl[]
|
downloadUrls: DownloadUrl[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
src/utils/category.ts
Normal file
27
src/utils/category.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Categories, Category } from 'types/category'
|
||||||
|
import categoriesData from './../assets/categories/categories.json'
|
||||||
|
|
||||||
|
const flattenCategories = (
|
||||||
|
categories: (Category | string)[],
|
||||||
|
parentPath: string[] = []
|
||||||
|
): Categories[] => {
|
||||||
|
return categories.flatMap<Categories, Category | string>((cat) => {
|
||||||
|
if (typeof cat === 'string') {
|
||||||
|
const path = [...parentPath, cat]
|
||||||
|
const hierarchy = path.join(' > ')
|
||||||
|
return [{ name: cat, hierarchy, l: path }]
|
||||||
|
} else {
|
||||||
|
const path = [...parentPath, cat.name]
|
||||||
|
const hierarchy = path.join(' > ')
|
||||||
|
if (cat.sub) {
|
||||||
|
const obj: Categories = { name: cat.name, hierarchy, l: path }
|
||||||
|
return [obj].concat(flattenCategories(cat.sub, path))
|
||||||
|
}
|
||||||
|
return [{ name: cat.name, hierarchy, l: path }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCategories = () => {
|
||||||
|
return flattenCategories(categoriesData)
|
||||||
|
}
|
@ -7,3 +7,4 @@ export * from './localStorage'
|
|||||||
export * from './consts'
|
export * from './consts'
|
||||||
export * from './blog'
|
export * from './blog'
|
||||||
export * from './curationSets'
|
export * from './curationSets'
|
||||||
|
export * from './category'
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { ModDetails, ModFormState } from '../types'
|
import { ModDetails, ModFormState } from '../types'
|
||||||
import { getTagValue } from './nostr'
|
import { getTagValue, getTagValues } from './nostr'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts and normalizes mod data from an event.
|
* Extracts and normalizes mod data from an event.
|
||||||
@ -43,6 +43,12 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
|
|||||||
originalAuthor: getFirstTagValue('originalAuthor'),
|
originalAuthor: getFirstTagValue('originalAuthor'),
|
||||||
screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [],
|
screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [],
|
||||||
tags: getTagValue(event, 'tags') || [],
|
tags: getTagValue(event, 'tags') || [],
|
||||||
|
LTags: (getTagValues(event, 'L') || []).map((t) =>
|
||||||
|
t.replace('com.degmods:', '')
|
||||||
|
),
|
||||||
|
lTags: (getTagValues(event, 'l') || []).map((t) =>
|
||||||
|
t.replace('com.degmods:', '')
|
||||||
|
),
|
||||||
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
|
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
|
||||||
JSON.parse(item)
|
JSON.parse(item)
|
||||||
)
|
)
|
||||||
@ -124,6 +130,8 @@ export const initializeFormState = (
|
|||||||
originalAuthor: existingModData?.originalAuthor || undefined,
|
originalAuthor: existingModData?.originalAuthor || undefined,
|
||||||
screenshotsUrls: existingModData?.screenshotsUrls || [''],
|
screenshotsUrls: existingModData?.screenshotsUrls || [''],
|
||||||
tags: existingModData?.tags.join(',') || '',
|
tags: existingModData?.tags.join(',') || '',
|
||||||
|
lTags: existingModData?.lTags || [],
|
||||||
|
LTags: existingModData?.LTags || [],
|
||||||
downloadUrls: existingModData?.downloadUrls || [
|
downloadUrls: existingModData?.downloadUrls || [
|
||||||
{
|
{
|
||||||
url: '',
|
url: '',
|
||||||
|
@ -156,3 +156,7 @@ export const parseFormData = <T>(formData: FormData) => {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const capitalizeEachWord = (str: string): string => {
|
||||||
|
return str.replace(/\b\w/g, (char) => char.toUpperCase())
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user