From 1ee56ba91ae185993fcb60d742f19a9529e85321 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 3 Dec 2024 15:09:40 +0100 Subject: [PATCH 01/21] feat: generic alert popup and nsfw popup confirmation --- src/components/AlertPopup.tsx | 72 ++++++++++++++++++++ src/components/Filters/BlogsFilter.tsx | 23 +------ src/components/Filters/ModsFilter.tsx | 16 +---- src/components/Filters/NsfwFilterOptions.tsx | 64 +++++++++++++++++ src/components/NsfwAlertPopup.tsx | 36 ++++++++++ src/components/ReportPopup.tsx | 4 +- src/hooks/index.ts | 1 + src/hooks/useSessionStorage.tsx | 71 +++++++++++++++++++ src/pages/blogs/index.tsx | 27 ++------ src/pages/settings/preference.tsx | 29 +++++++- src/types/index.ts | 1 + src/types/popup.ts | 9 +++ src/utils/index.ts | 1 + src/utils/sessionStorage.ts | 32 +++++++++ 14 files changed, 329 insertions(+), 57 deletions(-) create mode 100644 src/components/AlertPopup.tsx create mode 100644 src/components/Filters/NsfwFilterOptions.tsx create mode 100644 src/components/NsfwAlertPopup.tsx create mode 100644 src/hooks/useSessionStorage.tsx create mode 100644 src/types/popup.ts create mode 100644 src/utils/sessionStorage.ts diff --git a/src/components/AlertPopup.tsx b/src/components/AlertPopup.tsx new file mode 100644 index 0000000..e9334ee --- /dev/null +++ b/src/components/AlertPopup.tsx @@ -0,0 +1,72 @@ +import { createPortal } from 'react-dom' +import { AlertPopupProps } from 'types' + +export const AlertPopup = ({ + header, + label, + handleConfirm, + handleClose +}: AlertPopupProps) => { + return createPortal( +
+
+
+
+
+
+

{header}

+
+
+ + + +
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+
, + document.body + ) +} diff --git a/src/components/Filters/BlogsFilter.tsx b/src/components/Filters/BlogsFilter.tsx index 71b4098..5efc7d2 100644 --- a/src/components/Filters/BlogsFilter.tsx +++ b/src/components/Filters/BlogsFilter.tsx @@ -1,16 +1,11 @@ import { useAppSelector, useLocalStorage } from 'hooks' import React from 'react' -import { - FilterOptions, - ModeratedFilter, - NSFWFilter, - SortBy, - WOTFilterOptions -} from 'types' +import { FilterOptions, ModeratedFilter, SortBy, WOTFilterOptions } from 'types' import { DEFAULT_FILTER_OPTIONS } from 'utils' import { Dropdown } from './Dropdown' import { Option } from './Option' import { Filter } from '.' +import { NsfwFilterOptions } from './NsfwFilterOptions' type Props = { author?: string | undefined @@ -115,19 +110,7 @@ export const BlogsFilter = React.memo( {/* nsfw filter options */} - {Object.values(NSFWFilter).map((item, index) => ( - - ))} + {/* source filter options */} diff --git a/src/components/Filters/ModsFilter.tsx b/src/components/Filters/ModsFilter.tsx index afa43c4..8604db5 100644 --- a/src/components/Filters/ModsFilter.tsx +++ b/src/components/Filters/ModsFilter.tsx @@ -5,13 +5,13 @@ import { SortBy, ModeratedFilter, WOTFilterOptions, - NSFWFilter, RepostFilter } from 'types' import { DEFAULT_FILTER_OPTIONS } from 'utils' import { Filter } from '.' import { Dropdown } from './Dropdown' import { Option } from './Option' +import { NsfwFilterOptions } from './NsfwFilterOptions' type Props = { author?: string | undefined @@ -115,19 +115,7 @@ export const ModFilter = React.memo( {/* nsfw filter options */} - {Object.values(NSFWFilter).map((item, index) => ( - - ))} + {/* repost filter options */} diff --git a/src/components/Filters/NsfwFilterOptions.tsx b/src/components/Filters/NsfwFilterOptions.tsx new file mode 100644 index 0000000..71392f8 --- /dev/null +++ b/src/components/Filters/NsfwFilterOptions.tsx @@ -0,0 +1,64 @@ +import { FilterOptions, NSFWFilter } from 'types' +import { Option } from './Option' +import { NsfwAlertPopup } from 'components/NsfwAlertPopup' +import { useState } from 'react' +import { useLocalStorage, useSessionStorage } from 'hooks' +import { DEFAULT_FILTER_OPTIONS } from 'utils' + +interface NsfwFilterOptionsProps { + filterKey: string +} + +export const NsfwFilterOptions = ({ filterKey }: NsfwFilterOptionsProps) => { + const [, setFilterOptions] = useLocalStorage( + filterKey, + DEFAULT_FILTER_OPTIONS + ) + const [showNsfwPopup, setShowNsfwPopup] = useState(false) + const [selectedNsfwOption, setSelectedNsfwOption] = useState< + NSFWFilter | undefined + >() + const [confirmNsfw] = useSessionStorage('confirm-nsfw', false) + const handleConfirm = (confirm: boolean) => { + if (confirm && selectedNsfwOption) { + setFilterOptions((prev) => ({ + ...prev, + nsfw: selectedNsfwOption + })) + } + } + + return ( + <> + {Object.values(NSFWFilter).map((item, index) => ( + + ))} + {showNsfwPopup && ( + setShowNsfwPopup(false)} + /> + )} + + ) +} diff --git a/src/components/NsfwAlertPopup.tsx b/src/components/NsfwAlertPopup.tsx new file mode 100644 index 0000000..dbb9c60 --- /dev/null +++ b/src/components/NsfwAlertPopup.tsx @@ -0,0 +1,36 @@ +import { AlertPopupProps } from 'types' +import { AlertPopup } from './AlertPopup' +import { useSessionStorage } from 'hooks' + +type NsfwAlertPopup = Omit + +/** + * Triggers when the user wants to switch the filter to see any of the NSFW options + * (including preferences) + * + * Option will be remembered for the session only and will not show the popup again + */ +export const NsfwAlertPopup = ({ + handleConfirm, + handleClose +}: NsfwAlertPopup) => { + const [confirmNsfw, setConfirmNsfw] = useSessionStorage( + 'confirm-nsfw', + false + ) + + return ( + !confirmNsfw && ( + { + setConfirmNsfw(confirm) + handleConfirm(confirm) + handleClose() + }} + /> + ) + ) +} diff --git a/src/components/ReportPopup.tsx b/src/components/ReportPopup.tsx index d31e4b3..ace813d 100644 --- a/src/components/ReportPopup.tsx +++ b/src/components/ReportPopup.tsx @@ -3,12 +3,12 @@ import { CheckboxFieldUncontrolled } from 'components/Inputs' import { useEffect } from 'react' import { ReportReason } from 'types/report' import { LoadingSpinner } from './LoadingSpinner' +import { PopupProps } from 'types' type ReportPopupProps = { openedAt: number reasons: ReportReason[] - handleClose: () => void -} +} & PopupProps export const ReportPopup = ({ openedAt, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 3daf9f4..c01237e 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -8,3 +8,4 @@ export * from './useReactions' export * from './useNDKContext' export * from './useScrollDisable' export * from './useLocalStorage' +export * from './useSessionStorage' diff --git a/src/hooks/useSessionStorage.tsx b/src/hooks/useSessionStorage.tsx new file mode 100644 index 0000000..8f50422 --- /dev/null +++ b/src/hooks/useSessionStorage.tsx @@ -0,0 +1,71 @@ +import React from 'react' +import { + getSessionStorageItem, + removeSessionStorageItem, + setSessionStorageItem +} from 'utils' + +const useSessionStorageSubscribe = (callback: () => void) => { + window.addEventListener('sessionStorage', callback) + return () => window.removeEventListener('sessionStorage', callback) +} + +function mergeWithInitialValue(storedValue: T, initialValue: T): T { + if (typeof storedValue === 'object' && storedValue !== null) { + return { ...initialValue, ...storedValue } + } + return storedValue +} + +export function useSessionStorage( + key: string, + initialValue: T +): [T, React.Dispatch>] { + const getSnapshot = () => { + // Get the stored value + const storedValue = getSessionStorageItem(key, initialValue) + + // Parse the value + const parsedStoredValue = JSON.parse(storedValue) + + // Merge the default and the stored in case some of the required fields are missing + return JSON.stringify( + mergeWithInitialValue(parsedStoredValue, initialValue) + ) + } + + const data = React.useSyncExternalStore( + useSessionStorageSubscribe, + getSnapshot + ) + + const setState: React.Dispatch> = React.useCallback( + (v: React.SetStateAction) => { + try { + const nextState = + typeof v === 'function' + ? (v as (prevState: T) => T)(JSON.parse(data)) + : v + + if (nextState === undefined || nextState === null) { + removeSessionStorageItem(key) + } else { + setSessionStorageItem(key, JSON.stringify(nextState)) + } + } catch (e) { + console.warn(e) + } + }, + [data, key] + ) + + React.useEffect(() => { + // Set session storage only when it's empty + const data = window.sessionStorage.getItem(key) + if (data === null) { + setSessionStorageItem(key, JSON.stringify(initialValue)) + } + }, [key, initialValue]) + + return [JSON.parse(data) as T, setState] +} diff --git a/src/pages/blogs/index.tsx b/src/pages/blogs/index.tsx index 2155b39..d4b4fab 100644 --- a/src/pages/blogs/index.tsx +++ b/src/pages/blogs/index.tsx @@ -14,17 +14,16 @@ import { LoadingSpinner } from 'components/LoadingSpinner' import { Filter } from 'components/Filters' import { Dropdown } from 'components/Filters/Dropdown' import { Option } from 'components/Filters/Option' +import { NsfwFilterOptions } from 'components/Filters/NsfwFilterOptions' export const BlogsPage = () => { const navigation = useNavigation() const blogs = useLoaderData() as Partial[] | undefined - const [filterOptions, setFilterOptions] = useLocalStorage( - 'filter-blog-curated', - { - sort: SortBy.Latest, - nsfw: NSFWFilter.Hide_NSFW - } - ) + const filterKey = 'filter-blog-curated' + const [filterOptions, setFilterOptions] = useLocalStorage(filterKey, { + sort: SortBy.Latest, + nsfw: NSFWFilter.Hide_NSFW + }) // Search const searchTermRef = useRef(null) @@ -147,19 +146,7 @@ export const BlogsPage = () => { - {Object.values(NSFWFilter).map((item, index) => ( - - ))} + diff --git a/src/pages/settings/preference.tsx b/src/pages/settings/preference.tsx index 985a509..c2b0d42 100644 --- a/src/pages/settings/preference.tsx +++ b/src/pages/settings/preference.tsx @@ -1,6 +1,12 @@ import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk' import { LoadingSpinner } from 'components/LoadingSpinner' -import { useAppDispatch, useAppSelector, useNDKContext } from 'hooks' +import { NsfwAlertPopup } from 'components/NsfwAlertPopup' +import { + useAppDispatch, + useAppSelector, + useNDKContext, + useSessionStorage +} from 'hooks' import { kinds, UnsignedEvent, Event } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' @@ -19,6 +25,13 @@ export const PreferencesSetting = () => { const [wotLevel, setWotLevel] = useState(userWotLevel) const [isSaving, setIsSaving] = useState(false) + const [nsfw, setNsfw] = useState(false) + const [confirmNsfw] = useSessionStorage('confirm-nsfw', false) + const [showNsfwPopup, setShowNsfwPopup] = useState(false) + const handleNsfwConfirm = (confirm: boolean) => { + setNsfw(confirm) + } + useEffect(() => { if (user?.pubkey) { const hexPubkey = user.pubkey as string @@ -191,6 +204,14 @@ export const PreferencesSetting = () => { type='checkbox' className='CheckboxMain' name='NSFWPreference' + checked={nsfw} + onChange={(e) => { + if (e.currentTarget.checked && !confirmNsfw) { + setShowNsfwPopup(true) + } else { + setNsfw(e.currentTarget.checked) + } + }} /> @@ -238,6 +259,12 @@ export const PreferencesSetting = () => { Save + {showNsfwPopup && ( + setShowNsfwPopup(false)} + /> + )} diff --git a/src/types/index.ts b/src/types/index.ts index 8fe37df..64a1407 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,3 +4,4 @@ export * from './nostr' export * from './user' export * from './zap' export * from './blog' +export * from './popup' diff --git a/src/types/popup.ts b/src/types/popup.ts new file mode 100644 index 0000000..b1551cf --- /dev/null +++ b/src/types/popup.ts @@ -0,0 +1,9 @@ +export interface PopupProps { + handleClose: () => void +} + +export interface AlertPopupProps extends PopupProps { + header: string + label: string + handleConfirm: (confirm: boolean) => void +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 91fe37b..4e3c1d1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,6 +4,7 @@ export * from './url' export * from './utils' export * from './zap' export * from './localStorage' +export * from './sessionStorage' export * from './consts' export * from './blog' export * from './curationSets' diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts new file mode 100644 index 0000000..e40b0e9 --- /dev/null +++ b/src/utils/sessionStorage.ts @@ -0,0 +1,32 @@ +export function getSessionStorageItem(key: string, defaultValue: T): string { + try { + const data = window.sessionStorage.getItem(key) + if (data === null) return JSON.stringify(defaultValue) + return data + } catch (err) { + console.error(`Error while fetching session storage value: `, err) + return JSON.stringify(defaultValue) + } +} + +export function setSessionStorageItem(key: string, value: string) { + try { + window.sessionStorage.setItem(key, value) + dispatchSessionStorageEvent(key, value) + } catch (err) { + console.error(`Error while saving session storage value: `, err) + } +} + +export function removeSessionStorageItem(key: string) { + try { + window.sessionStorage.removeItem(key) + dispatchSessionStorageEvent(key, null) + } catch (err) { + console.error(`Error while deleting session storage value: `, err) + } +} + +function dispatchSessionStorageEvent(key: string, newValue: string | null) { + window.dispatchEvent(new StorageEvent('sessionStorage', { key, newValue })) +} -- 2.34.1 From 71f934129c2543cc71811854fd4498a7cb0e82d3 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 3 Dec 2024 17:32:55 +0100 Subject: [PATCH 02/21] refactor: use local storage instead of session for nsfw preference --- src/components/Filters/NsfwFilterOptions.tsx | 4 ++-- src/components/NsfwAlertPopup.tsx | 4 ++-- src/pages/settings/preference.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/Filters/NsfwFilterOptions.tsx b/src/components/Filters/NsfwFilterOptions.tsx index 71392f8..05ab906 100644 --- a/src/components/Filters/NsfwFilterOptions.tsx +++ b/src/components/Filters/NsfwFilterOptions.tsx @@ -2,7 +2,7 @@ import { FilterOptions, NSFWFilter } from 'types' import { Option } from './Option' import { NsfwAlertPopup } from 'components/NsfwAlertPopup' import { useState } from 'react' -import { useLocalStorage, useSessionStorage } from 'hooks' +import { useLocalStorage } from 'hooks' import { DEFAULT_FILTER_OPTIONS } from 'utils' interface NsfwFilterOptionsProps { @@ -18,7 +18,7 @@ export const NsfwFilterOptions = ({ filterKey }: NsfwFilterOptionsProps) => { const [selectedNsfwOption, setSelectedNsfwOption] = useState< NSFWFilter | undefined >() - const [confirmNsfw] = useSessionStorage('confirm-nsfw', false) + const [confirmNsfw] = useLocalStorage('confirm-nsfw', false) const handleConfirm = (confirm: boolean) => { if (confirm && selectedNsfwOption) { setFilterOptions((prev) => ({ diff --git a/src/components/NsfwAlertPopup.tsx b/src/components/NsfwAlertPopup.tsx index dbb9c60..1685a97 100644 --- a/src/components/NsfwAlertPopup.tsx +++ b/src/components/NsfwAlertPopup.tsx @@ -1,6 +1,6 @@ import { AlertPopupProps } from 'types' import { AlertPopup } from './AlertPopup' -import { useSessionStorage } from 'hooks' +import { useLocalStorage } from 'hooks' type NsfwAlertPopup = Omit @@ -14,7 +14,7 @@ export const NsfwAlertPopup = ({ handleConfirm, handleClose }: NsfwAlertPopup) => { - const [confirmNsfw, setConfirmNsfw] = useSessionStorage( + const [confirmNsfw, setConfirmNsfw] = useLocalStorage( 'confirm-nsfw', false ) diff --git a/src/pages/settings/preference.tsx b/src/pages/settings/preference.tsx index c2b0d42..7f6c8e0 100644 --- a/src/pages/settings/preference.tsx +++ b/src/pages/settings/preference.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector, useNDKContext, - useSessionStorage + useLocalStorage } from 'hooks' import { kinds, UnsignedEvent, Event } from 'nostr-tools' import { useEffect, useState } from 'react' @@ -26,7 +26,7 @@ export const PreferencesSetting = () => { const [isSaving, setIsSaving] = useState(false) const [nsfw, setNsfw] = useState(false) - const [confirmNsfw] = useSessionStorage('confirm-nsfw', false) + const [confirmNsfw] = useLocalStorage('confirm-nsfw', false) const [showNsfwPopup, setShowNsfwPopup] = useState(false) const handleNsfwConfirm = (confirm: boolean) => { setNsfw(confirm) -- 2.34.1 From 3b2dce54c5b36deba62c60de42a6af481912e256 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 3 Dec 2024 10:47:55 +0100 Subject: [PATCH 03/21] feat: initial categories --- src/assets/categories/categories.json | 24 +++ src/components/ModForm.tsx | 249 +++++++++++++++++++++++++- src/pages/mod/index.tsx | 33 ++++ src/pages/search.tsx | 48 ++++- src/types/category.ts | 12 ++ src/types/index.ts | 1 + src/types/mod.ts | 4 + src/utils/category.ts | 27 +++ src/utils/index.ts | 1 + src/utils/mod.ts | 10 +- src/utils/utils.ts | 4 + 11 files changed, 409 insertions(+), 4 deletions(-) create mode 100644 src/assets/categories/categories.json create mode 100644 src/types/category.ts create mode 100644 src/utils/category.ts diff --git a/src/assets/categories/categories.json b/src/assets/categories/categories.json new file mode 100644 index 0000000..2948280 --- /dev/null +++ b/src/assets/categories/categories.json @@ -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" +] diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index ea54912..4fee8a0 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -16,8 +16,10 @@ import { T_TAG_VALUE } from '../constants' import { useAppSelector, useGames, useNDKContext } from '../hooks' import { appRoutes, getModPageRoute } from '../routes' import '../styles/styles.css' -import { DownloadUrl, ModDetails, ModFormState } from '../types' +import { Categories, DownloadUrl, ModDetails, ModFormState } from '../types' import { + capitalizeEachWord, + getCategories, initializeFormState, isReachable, isValidImageUrl, @@ -231,6 +233,21 @@ export const ModForm = ({ existingModData }: ModFormProps) => { 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 = { kind: kinds.ClassifiedListing, created_at: currentTimeStamp, @@ -489,6 +506,11 @@ export const ModForm = ({ existingModData }: ModFormProps) => { error={formErrors.tags} onChange={handleInputChange} /> +
@@ -532,7 +554,6 @@ export const ModForm = ({ existingModData }: ModFormProps) => { )} ))} - {formState.downloadUrls.length === 0 && formErrors.downloadUrls && formErrors.downloadUrls[0] && ( @@ -878,3 +899,227 @@ const GameDropdown = ({
) } + +interface CategoryAutocompleteProps { + lTags: string[] + LTags: string[] + setFormState: (value: React.SetStateAction) => 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(initialCategories) + const [inputValue, setInputValue] = useState('') + const [filteredOptions, setFilteredOptions] = useState() + + 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((prev, cat) => [...prev, ...cat.l], []) + ) + const concatenatedValue = Array.from(uniqueValues) + return concatenatedValue + } + const getSelectedHeirarchy = (cats: Categories[]) => { + const heirarchies = cats.reduce( + (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) => { + 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 ( +
+ +

You can select multiple categories

+
+
+ + + +
+ {filteredOptions && filteredOptions.length > 0 ? ( + + {({ index, style }) => ( +
handleSelect(filteredOptions[index])} + > + {capitalizeEachWord(filteredOptions[index].hierarchy)} + {selectedCategories.some( + (cat) => + cat.hierarchy === filteredOptions[index].hierarchy + ) && ( + + )} +
+ )} +
+ ) : ( + + {({ index, style }) => ( +
+ {inputValue && + !filteredOptions?.find( + (option) => + option.hierarchy.toLowerCase() === + inputValue.toLowerCase() + ) ? ( + <> + Add "{inputValue}" + + + ) : ( + <>No matches + )} +
+ )} +
+ )} +
+
+
+ {LTags.length > 0 && ( +
+ {LTags.map((hierarchy) => { + const heirarchicalCategories = hierarchy.split(`:`) + const categories = heirarchicalCategories + .map((c: string) => ( + +

{capitalizeEachWord(c)}

+
+ )) + .reduce((prev, curr) => [ + prev, +
+

>

+
, + curr + ]) + + return ( +
+ {categories} +
+ ) + })} +
+ )} +
+ ) +} diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx index cc77765..82587fc 100644 --- a/src/pages/mod/index.tsx +++ b/src/pages/mod/index.tsx @@ -105,6 +105,8 @@ export const ModPage = () => { body={mod.body} screenshotsUrls={mod.screenshotsUrls} tags={mod.tags} + LTags={mod.LTags} + lTags={mod.lTags} nsfw={mod.nsfw} repost={mod.repost} originalAuthor={mod.originalAuthor} @@ -426,6 +428,8 @@ type BodyProps = { body: string screenshotsUrls: string[] tags: string[] + LTags: string[] + lTags: string[] nsfw: boolean repost: boolean originalAuthor?: string @@ -437,6 +441,8 @@ const Body = ({ body, screenshotsUrls, tags, + LTags, + lTags, nsfw, repost, originalAuthor @@ -532,6 +538,33 @@ const Body = ({ {tag} ))} + + {LTags.length > 0 && ( +
+ {LTags.map((hierarchy) => { + const heirarchicalCategories = hierarchy.split(`:`) + const categories = heirarchicalCategories + .map((c: string) => ( + +

{c}

+
+ )) + .reduce((prev, curr) => [ + prev, +
+

>

+
, + curr + ]) + + return ( +
+ {categories} +
+ ) + })} +
+ )}
diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 683696f..4901f1e 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -43,7 +43,8 @@ import { useCuratedSet } from 'hooks/useCuratedSet' enum SearchKindEnum { Mods = 'Mods', Games = 'Games', - Users = 'Users' + Users = 'Users', + Categories = 'Categories' } export const SearchPage = () => { @@ -132,6 +133,10 @@ export const SearchPage = () => { {searchKind === SearchKindEnum.Games && ( )} + + {searchKind === SearchKindEnum.Categories && ( + + )} @@ -538,3 +543,44 @@ function dedup(event1: NDKEvent, event2: NDKEvent) { return event2 } + +interface CategoriesResultProps { + searchTerm: string +} + +const CategoriesResult = ({ searchTerm }: CategoriesResultProps) => { + const { ndk } = useNDKContext() + const [mods, setMods] = useState() + + 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 ( +
+
+ {mods?.map((mod) => ( + + ))} +
+
+ ) +} diff --git a/src/types/category.ts b/src/types/category.ts new file mode 100644 index 0000000..4d871f8 --- /dev/null +++ b/src/types/category.ts @@ -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[] +} diff --git a/src/types/index.ts b/src/types/index.ts index 8fe37df..d26ffe0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,3 +4,4 @@ export * from './nostr' export * from './user' export * from './zap' export * from './blog' +export * from './category' diff --git a/src/types/mod.ts b/src/types/mod.ts index bc27e18..c520ac3 100644 --- a/src/types/mod.ts +++ b/src/types/mod.ts @@ -31,6 +31,10 @@ export interface ModFormState { originalAuthor?: string screenshotsUrls: string[] tags: string + /** Hierarchical labels */ + LTags: string[] + /** Category labels for category search */ + lTags: string[] downloadUrls: DownloadUrl[] } diff --git a/src/utils/category.ts b/src/utils/category.ts new file mode 100644 index 0000000..6aef63f --- /dev/null +++ b/src/utils/category.ts @@ -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((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) +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 91fe37b..f6de84d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,3 +7,4 @@ export * from './localStorage' export * from './consts' export * from './blog' export * from './curationSets' +export * from './category' diff --git a/src/utils/mod.ts b/src/utils/mod.ts index 24b59bd..07f34a0 100644 --- a/src/utils/mod.ts +++ b/src/utils/mod.ts @@ -1,7 +1,7 @@ import { NDKEvent } from '@nostr-dev-kit/ndk' import { Event } from 'nostr-tools' import { ModDetails, ModFormState } from '../types' -import { getTagValue } from './nostr' +import { getTagValue, getTagValues } from './nostr' /** * Extracts and normalizes mod data from an event. @@ -43,6 +43,12 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => { originalAuthor: getFirstTagValue('originalAuthor'), screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [], 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) => JSON.parse(item) ) @@ -124,6 +130,8 @@ export const initializeFormState = ( originalAuthor: existingModData?.originalAuthor || undefined, screenshotsUrls: existingModData?.screenshotsUrls || [''], tags: existingModData?.tags.join(',') || '', + lTags: existingModData?.lTags || [], + LTags: existingModData?.LTags || [], downloadUrls: existingModData?.downloadUrls || [ { url: '', diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 4658496..bb740e5 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -156,3 +156,7 @@ export const parseFormData = (formData: FormData) => { return result } + +export const capitalizeEachWord = (str: string): string => { + return str.replace(/\b\w/g, (char) => char.toUpperCase()) +} -- 2.34.1 From cb94f0ced6cc6d9717b18c00ed3e20cc6418bcb2 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 3 Dec 2024 19:17:53 +0100 Subject: [PATCH 04/21] fix(search): remove test categoriesh --- src/pages/search.tsx | 48 +------------------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 4901f1e..683696f 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -43,8 +43,7 @@ import { useCuratedSet } from 'hooks/useCuratedSet' enum SearchKindEnum { Mods = 'Mods', Games = 'Games', - Users = 'Users', - Categories = 'Categories' + Users = 'Users' } export const SearchPage = () => { @@ -133,10 +132,6 @@ export const SearchPage = () => { {searchKind === SearchKindEnum.Games && ( )} - - {searchKind === SearchKindEnum.Categories && ( - - )} @@ -543,44 +538,3 @@ function dedup(event1: NDKEvent, event2: NDKEvent) { return event2 } - -interface CategoriesResultProps { - searchTerm: string -} - -const CategoriesResult = ({ searchTerm }: CategoriesResultProps) => { - const { ndk } = useNDKContext() - const [mods, setMods] = useState() - - 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 ( -
-
- {mods?.map((mod) => ( - - ))} -
-
- ) -} -- 2.34.1 From cd5e6dcd8fbc7e381777522244f56749478e7ed1 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 3 Dec 2024 19:27:51 +0100 Subject: [PATCH 05/21] feat(categories): link c to games and split input on > --- src/components/ModForm.tsx | 27 ++++++++++++++++++++------- src/pages/mod/index.tsx | 19 +++++++++++++------ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index 4fee8a0..df735cd 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -8,13 +8,13 @@ import React, { useRef, useState } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' +import { Link, useLocation, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' import { FixedSizeList as List } from 'react-window' import { v4 as uuidv4 } from 'uuid' import { T_TAG_VALUE } from '../constants' import { useAppSelector, useGames, useNDKContext } from '../hooks' -import { appRoutes, getModPageRoute } from '../routes' +import { appRoutes, getGamePageRoute, getModPageRoute } from '../routes' import '../styles/styles.css' import { Categories, DownloadUrl, ModDetails, ModFormState } from '../types' import { @@ -256,6 +256,9 @@ export const ModForm = ({ existingModData }: ModFormProps) => { tags } + console.log(unsignedEvent) + return + const signedEvent = await window.nostr ?.signEvent(unsignedEvent) .then((event) => event as Event) @@ -507,6 +510,7 @@ export const ModForm = ({ existingModData }: ModFormProps) => { onChange={handleInputChange} /> ) => void } export const CategoryAutocomplete = ({ + game, lTags, LTags, setFormState @@ -970,10 +976,11 @@ export const CategoryAutocomplete = ({ } const handleAddNew = () => { if (inputValue) { + const value = inputValue.trim() const newOption: Categories = { - name: inputValue, - hierarchy: inputValue, - l: [inputValue] + name: value, + hierarchy: value, + l: value.split('>').map((s) => s.trim()) } setSelectedCategories([...selectedCategories, newOption]) setInputValue('') @@ -1100,9 +1107,15 @@ export const CategoryAutocomplete = ({ const heirarchicalCategories = hierarchy.split(`:`) const categories = heirarchicalCategories .map((c: string) => ( - +

{capitalizeEachWord(c)}

-
+ )) .reduce((prev, curr) => [ prev, diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx index 82587fc..e8f0831 100644 --- a/src/pages/mod/index.tsx +++ b/src/pages/mod/index.tsx @@ -28,6 +28,7 @@ import '../../styles/tags.css' import '../../styles/write.css' import { DownloadUrl, ModPageLoaderResult } from '../../types' import { + capitalizeEachWord, copyTextToClipboard, downloadFile, getFilenameFromUrl @@ -103,10 +104,10 @@ export const ModPage = () => { featuredImageUrl={mod.featuredImageUrl} title={mod.title} body={mod.body} + game={mod.game} screenshotsUrls={mod.screenshotsUrls} tags={mod.tags} LTags={mod.LTags} - lTags={mod.lTags} nsfw={mod.nsfw} repost={mod.repost} originalAuthor={mod.originalAuthor} @@ -426,10 +427,10 @@ type BodyProps = { featuredImageUrl: string title: string body: string + game: string screenshotsUrls: string[] tags: string[] LTags: string[] - lTags: string[] nsfw: boolean repost: boolean originalAuthor?: string @@ -437,12 +438,12 @@ type BodyProps = { const Body = ({ featuredImageUrl, + game, title, body, screenshotsUrls, tags, LTags, - lTags, nsfw, repost, originalAuthor @@ -545,9 +546,15 @@ const Body = ({ const heirarchicalCategories = hierarchy.split(`:`) const categories = heirarchicalCategories .map((c: string) => ( - -

{c}

-
+ +

{capitalizeEachWord(c)}

+
)) .reduce((prev, curr) => [ prev, -- 2.34.1 From 4bf84cd9a6379bfd3a202ce1706d0d98529b8ff6 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 3 Dec 2024 19:28:46 +0100 Subject: [PATCH 06/21] fix(mod): remove debug code --- src/components/ModForm.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index df735cd..8546125 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -256,9 +256,6 @@ export const ModForm = ({ existingModData }: ModFormProps) => { tags } - console.log(unsignedEvent) - return - const signedEvent = await window.nostr ?.signEvent(unsignedEvent) .then((event) => event as Event) -- 2.34.1 From 836d5b76e1d36d9eb2f5e0108fb8f8e090d11297 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 4 Dec 2024 13:26:22 +0100 Subject: [PATCH 07/21] feat(category): dynamic dropdown item height --- src/components/ModForm.tsx | 126 +++++++++++++++++++++++++------------ src/styles/styles.css | 4 +- 2 files changed, 89 insertions(+), 41 deletions(-) diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index 8546125..67e1f3f 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -1,6 +1,7 @@ import _ from 'lodash' import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools' import React, { + CSSProperties, Fragment, useCallback, useEffect, @@ -10,7 +11,7 @@ import React, { } from 'react' import { Link, useLocation, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' -import { FixedSizeList as List } from 'react-window' +import { VariableSizeList, FixedSizeList } from 'react-window' import { v4 as uuidv4 } from 'uuid' import { T_TAG_VALUE } from '../constants' import { useAppSelector, useGames, useNDKContext } from '../hooks' @@ -865,7 +866,7 @@ const GameDropdown = ({
- )} - +
@@ -992,6 +993,70 @@ export const CategoryAutocomplete = ({ })) }, [selectedCategories, setFormState]) + const listRef = useRef(null) + const rowHeights = useRef<{ [index: number]: number }>({}) + const setRowHeight = (index: number, size: number) => { + rowHeights.current = { ...rowHeights.current, [index]: size } + if (listRef.current) { + listRef.current.resetAfterIndex(index) + } + } + const getRowHeight = (index: number) => { + return (rowHeights.current[index] || 35) + 8 + } + + const Row = ({ index, style }: { index: number; style: CSSProperties }) => { + const rowRef = useRef(null) + + useEffect(() => { + const rowElement = rowRef.current + if (!rowElement) return + const updateHeight = () => { + const height = Math.max(rowElement.scrollHeight, 35) + setRowHeight(index, height) + } + const observer = new ResizeObserver(() => { + updateHeight() + }) + observer.observe(rowElement) + updateHeight() + return () => { + observer.disconnect() + } + }, [index]) + + if (!filteredOptions) return null + + return ( +
handleSelect(filteredOptions[index])} + > + {capitalizeEachWord(filteredOptions[index].hierarchy)} + {selectedCategories.some( + (cat) => cat.hierarchy === filteredOptions[index].hierarchy + ) && ( + + )} +
+ ) + } + return (
@@ -1025,43 +1090,22 @@ export const CategoryAutocomplete = ({
{filteredOptions && filteredOptions.length > 0 ? ( - + {Row} + + ) : ( + - {({ index, style }) => ( -
handleSelect(filteredOptions[index])} - > - {capitalizeEachWord(filteredOptions[index].hierarchy)} - {selectedCategories.some( - (cat) => - cat.hierarchy === filteredOptions[index].hierarchy - ) && ( - - )} -
- )} -
- ) : ( - {({ index, style }) => (
)} - + )}
@@ -1103,8 +1147,9 @@ export const CategoryAutocomplete = ({ {LTags.map((hierarchy) => { const heirarchicalCategories = hierarchy.split(`:`) const categories = heirarchicalCategories - .map((c: string) => ( + .map((c, i) => ( {capitalizeEachWord(c)}

)) - .reduce((prev, curr) => [ + .reduce((prev, curr, i) => [ prev, -
+

>

, curr diff --git a/src/styles/styles.css b/src/styles/styles.css index 8458aae..6aad09b 100644 --- a/src/styles/styles.css +++ b/src/styles/styles.css @@ -298,7 +298,7 @@ h6 { } .dropdownMainMenuItem { - transition: ease 0.4s; + transition: background ease 0.4s, color ease 0.4s; background: linear-gradient( rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.03) @@ -319,7 +319,7 @@ h6 { } .dropdownMainMenuItem:hover { - transition: ease 0.4s; + transition: background ease 0.4s, color ease 0.4s; background: linear-gradient( rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05) -- 2.34.1 From 1454929710cf65a3da51fb675a291f68ec310bf6 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 4 Dec 2024 14:16:39 +0100 Subject: [PATCH 08/21] feat(category): initial filter prep --- .../Filters/CategoryFilterPopup.tsx | 3 ++ src/components/Filters/ModsFilter.tsx | 6 ++- src/components/ModForm.tsx | 2 +- src/pages/game.tsx | 43 ++++++++++++++++++- src/pages/mod/index.tsx | 2 +- 5 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 src/components/Filters/CategoryFilterPopup.tsx diff --git a/src/components/Filters/CategoryFilterPopup.tsx b/src/components/Filters/CategoryFilterPopup.tsx new file mode 100644 index 0000000..2257fa6 --- /dev/null +++ b/src/components/Filters/CategoryFilterPopup.tsx @@ -0,0 +1,3 @@ +export const CategoryFilterPopup = () => { + return <>Popup +} diff --git a/src/components/Filters/ModsFilter.tsx b/src/components/Filters/ModsFilter.tsx index afa43c4..3d72d0c 100644 --- a/src/components/Filters/ModsFilter.tsx +++ b/src/components/Filters/ModsFilter.tsx @@ -1,5 +1,5 @@ import { useAppSelector, useLocalStorage } from 'hooks' -import React from 'react' +import React, { PropsWithChildren } from 'react' import { FilterOptions, SortBy, @@ -19,7 +19,7 @@ type Props = { } export const ModFilter = React.memo( - ({ author, filterKey = 'filter' }: Props) => { + ({ author, filterKey = 'filter', children }: PropsWithChildren) => { const userState = useAppSelector((state) => state.user) const [filterOptions, setFilterOptions] = useLocalStorage( filterKey, @@ -176,6 +176,8 @@ export const ModFilter = React.memo( Show All + + {children} ) } diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index 67e1f3f..7cd965b 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -1152,7 +1152,7 @@ export const CategoryAutocomplete = ({ key={`category-${i}`} to={{ pathname: getGamePageRoute(game), - search: `c=${c}` + search: `l=${c}` }} className='IBMSMSMBSSCategoriesBoxItem' > diff --git a/src/pages/game.tsx b/src/pages/game.tsx index da49f87..ea81648 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -27,6 +27,7 @@ import { scrollIntoView } from 'utils' import { useCuratedSet } from 'hooks/useCuratedSet' +import { CategoryFilterPopup } from 'components/Filters/CategoryFilterPopup' export const GamePage = () => { const scrollTargetRef = useRef(null) @@ -52,6 +53,13 @@ export const GamePage = () => { const [searchParams, setSearchParams] = useSearchParams() const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '') + // Categories filter + const [categories, setCategories] = useState(searchParams.getAll('l') || []) + const [heirarchies, setFullHeirarchies] = useState( + searchParams.getAll('h') || [] + ) + const [showCategoryPopup, setShowCategoryPopup] = useState(false) + const handleSearch = () => { const value = searchTermRef.current?.value || '' // Access the input value from the ref setSearchTerm(value) @@ -128,6 +136,14 @@ export const GamePage = () => { '#t': [T_TAG_VALUE] } + if (categories.length) { + filter['#l'] = categories.map((l) => `com.degmods:${l}`) + } + + if (heirarchies.length) { + filter['#L'] = heirarchies.map((L) => `com.degmods:${L}`) + } + const subscription = ndk.subscribe(filter, { cacheUsage: NDKSubscriptionCacheUsage.PARALLEL, closeOnEose: true @@ -151,7 +167,7 @@ export const GamePage = () => { return () => { subscription.stop() } - }, [gameName, ndk]) + }, [gameName, ndk, categories, heirarchies]) if (!gameName) return null @@ -188,7 +204,29 @@ export const GamePage = () => { />
- + +
+ +
+
+
{currentMods.map((mod) => ( @@ -204,6 +242,7 @@ export const GamePage = () => {
+ {showCategoryPopup && } ) } diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx index e8f0831..4006586 100644 --- a/src/pages/mod/index.tsx +++ b/src/pages/mod/index.tsx @@ -550,7 +550,7 @@ const Body = ({ className='IBMSMSMBSSCategoriesBoxItem' to={{ pathname: getGamePageRoute(game), - search: `c=${c}` + search: `l=${c}` }} >

{capitalizeEachWord(c)}

-- 2.34.1 From 8d9bbbc7a5e00e84b72f19cff1cc5c9f3fb6ab94 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 5 Dec 2024 13:02:04 +0100 Subject: [PATCH 09/21] feat(category): category filter popup --- .../Filters/CategoryFilterPopup.module.scss | 3 + .../Filters/CategoryFilterPopup.tsx | 372 +++++++++++++++++- src/pages/game.tsx | 34 +- 3 files changed, 403 insertions(+), 6 deletions(-) create mode 100644 src/components/Filters/CategoryFilterPopup.module.scss diff --git a/src/components/Filters/CategoryFilterPopup.module.scss b/src/components/Filters/CategoryFilterPopup.module.scss new file mode 100644 index 0000000..752a21e --- /dev/null +++ b/src/components/Filters/CategoryFilterPopup.module.scss @@ -0,0 +1,3 @@ +.noResult:not(:only-child) { + display: none; +} diff --git a/src/components/Filters/CategoryFilterPopup.tsx b/src/components/Filters/CategoryFilterPopup.tsx index 2257fa6..06f287e 100644 --- a/src/components/Filters/CategoryFilterPopup.tsx +++ b/src/components/Filters/CategoryFilterPopup.tsx @@ -1,3 +1,371 @@ -export const CategoryFilterPopup = () => { - return <>Popup +import React, { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' +import { Category } from 'types' +import categoriesData from './../../assets/categories/categories.json' +import { capitalizeEachWord } from 'utils' + +import styles from './CategoryFilterPopup.module.scss' + +interface CategoryFilterPopupProps { + categories: string[] + setCategories: React.Dispatch> + heirarchies: string[] + setHeirarchies: React.Dispatch> + handleClose: () => void + handleApply: () => void +} + +export const CategoryFilterPopup = ({ + categories, + setCategories, + heirarchies, + setHeirarchies, + handleClose, + handleApply +}: CategoryFilterPopupProps) => { + const [inputValue, setInputValue] = useState('') + const handleInputChange = (e: React.ChangeEvent) => { + setInputValue(e.target.value) + } + const handleSingleSelection = (category: string, isSelected: boolean) => { + let updatedCategories = [...categories] + if (isSelected) { + updatedCategories.push(category) + } else { + updatedCategories = updatedCategories.filter((item) => item !== category) + } + setCategories(updatedCategories) + } + const handleCombinationSelection = (path: string[], isSelected: boolean) => { + const pathString = path.join(':') + let updatedHeirarchies = [...heirarchies] + if (isSelected) { + updatedHeirarchies.push(pathString) + } else { + updatedHeirarchies = updatedHeirarchies.filter( + (item) => item !== pathString + ) + } + setHeirarchies(updatedHeirarchies) + } + const handleAddNew = () => { + if (inputValue) { + const values = inputValue + .trim() + .split('>') + .map((s) => s.trim()) + if (values.length > 1) { + setHeirarchies([...categories, values.join(':')]) + } else { + setCategories([...categories, values[0]]) + } + setInputValue('') + } + } + + return createPortal( +
+
+
+
+
+
+

Categories filter

+
+
+ + + +
+
+
+
+
+ +

+ This is description for an input and how to use search here +

+
+ + {true && ( +
+ +

Maybe

+
+ )} +
+
+
+
No results.
+
+
+ Search for "{inputValue}" category + +
+
+ {(categoriesData as Category[]).map((category) => ( + + ))} +
+
+
+ + + +
+
+
+
+
+
+
, + document.body + ) +} + +interface CategoryCheckboxProps { + inputValue: string + category: Category | string + path: string[] + handleSingleSelection: (category: string, isSelected: boolean) => void + handleCombinationSelection: (path: string[], isSelected: boolean) => void + selectedSingles: string[] + selectedCombinations: string[] + indentLevel?: number +} + +const CategoryCheckbox: React.FC = ({ + inputValue, + category, + path, + handleSingleSelection, + handleCombinationSelection, + selectedSingles, + selectedCombinations, + indentLevel = 0 +}) => { + const name = typeof category === 'string' ? category : category.name + const isMatching = path + .join(' > ') + .toLowerCase() + .includes(inputValue.toLowerCase()) + const [isSingleChecked, setIsSingleChecked] = useState(false) + const [isCombinationChecked, setIsCombinationChecked] = + useState(false) + const [isIndeterminate, setIsIndeterminate] = useState(false) + + useEffect(() => { + const pathString = path.join(':') + setIsSingleChecked(selectedSingles.includes(name)) + setIsCombinationChecked(selectedCombinations.includes(pathString)) + + const childPaths = + category.sub && Array.isArray(category.sub) + ? category.sub.map((sub) => + typeof sub === 'string' + ? [...path, sub].join(':') + : [...path, sub.name].join(':') + ) + : [] + const anyChildCombinationSelected = childPaths.some((childPath) => + selectedCombinations.includes(childPath) + ) + + if ( + anyChildCombinationSelected && + !selectedCombinations.includes(pathString) + ) { + setIsIndeterminate(true) + } else { + setIsIndeterminate(false) + } + }, [selectedSingles, selectedCombinations, path, name, category.sub]) + + const handleSingleChange = () => { + setIsSingleChecked(!isSingleChecked) + handleSingleSelection(name, !isSingleChecked) + } + + const handleCombinationChange = () => { + setIsCombinationChecked(!isCombinationChecked) + handleCombinationSelection(path, !isCombinationChecked) + } + + return ( + <> + {isMatching && ( +
+
+ { + if (input) { + input.indeterminate = isIndeterminate + } + }} + className='CheckboxMain' + checked={isCombinationChecked} + onChange={handleCombinationChange} + /> + + +
+
+ )} + {typeof category !== 'string' && + category.sub && + Array.isArray(category.sub) && ( + <> + {category.sub.map((subCategory) => { + if (typeof subCategory === 'string') { + return ( + + ) + } else { + return ( + + ) + } + })} + + )} + + ) } diff --git a/src/pages/game.tsx b/src/pages/game.tsx index ea81648..9c8d7d6 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -55,9 +55,7 @@ export const GamePage = () => { // Categories filter const [categories, setCategories] = useState(searchParams.getAll('l') || []) - const [heirarchies, setFullHeirarchies] = useState( - searchParams.getAll('h') || [] - ) + const [heirarchies, setHeirarchies] = useState(searchParams.getAll('h') || []) const [showCategoryPopup, setShowCategoryPopup] = useState(false) const handleSearch = () => { @@ -242,7 +240,35 @@ export const GamePage = () => { - {showCategoryPopup && } + {showCategoryPopup && ( + { + setShowCategoryPopup(false) + }} + handleApply={() => { + searchParams.delete('l') + searchParams.delete('h') + categories.forEach((l) => { + if (l) { + searchParams.delete('h') + searchParams.append('l', l) + } + }) + heirarchies.forEach((h) => { + if (h) { + searchParams.append('h', h) + } + }) + setSearchParams(searchParams, { + replace: true + }) + }} + /> + )} ) } -- 2.34.1 From 41cfc57cf901b065625b1337af076853951567ca Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 5 Dec 2024 13:37:30 +0100 Subject: [PATCH 10/21] fix(category): open in new tab, require game select --- src/components/ModForm.tsx | 92 ++++++++++++++++++-------------------- src/pages/mod/index.tsx | 1 + 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index 7cd965b..9711ac4 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -1100,44 +1100,33 @@ export const CategoryAutocomplete = ({ {Row}
) : ( - - {({ index, style }) => ( -
- {inputValue && - !filteredOptions?.find( - (option) => - option.hierarchy.toLowerCase() === - inputValue.toLowerCase() - ) ? ( - <> - Add "{inputValue}" - - - ) : ( - <>No matches - )} -
+ {inputValue && + !filteredOptions?.find( + (option) => + option.hierarchy.toLowerCase() === inputValue.toLowerCase() + ) ? ( + <> + Add "{inputValue}" + + + ) : ( + <>No matches )} -
+ )} @@ -1147,18 +1136,25 @@ export const CategoryAutocomplete = ({ {LTags.map((hierarchy) => { const heirarchicalCategories = hierarchy.split(`:`) const categories = heirarchicalCategories - .map((c, i) => ( - -

{capitalizeEachWord(c)}

- - )) + .map((c, i) => + game ? ( + +

{capitalizeEachWord(c)}

+ + ) : ( +

+ {capitalizeEachWord(c)} +

+ ) + ) .reduce((prev, curr, i) => [ prev,
((c: string) => ( Date: Thu, 5 Dec 2024 20:51:02 +0100 Subject: [PATCH 11/21] fix(storage): memoize hook values after JSON parsing --- src/hooks/useLocalStorage.tsx | 12 +++++++++--- src/hooks/useSessionStorage.tsx | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/hooks/useLocalStorage.tsx b/src/hooks/useLocalStorage.tsx index 4d1eac2..10579cd 100644 --- a/src/hooks/useLocalStorage.tsx +++ b/src/hooks/useLocalStorage.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import { getLocalStorageItem, removeLocalStorageItem, @@ -11,7 +11,11 @@ const useLocalStorageSubscribe = (callback: () => void) => { } function mergeWithInitialValue(storedValue: T, initialValue: T): T { - if (typeof storedValue === 'object' && storedValue !== null) { + if ( + !Array.isArray(storedValue) && + typeof storedValue === 'object' && + storedValue !== null + ) { return { ...initialValue, ...storedValue } } return storedValue @@ -64,5 +68,7 @@ export function useLocalStorage( } }, [key, initialValue]) - return [JSON.parse(data) as T, setState] + const memoized = useMemo(() => JSON.parse(data) as T, [data]) + + return [memoized, setState] } diff --git a/src/hooks/useSessionStorage.tsx b/src/hooks/useSessionStorage.tsx index 8f50422..cc0756f 100644 --- a/src/hooks/useSessionStorage.tsx +++ b/src/hooks/useSessionStorage.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import { getSessionStorageItem, removeSessionStorageItem, @@ -11,7 +11,11 @@ const useSessionStorageSubscribe = (callback: () => void) => { } function mergeWithInitialValue(storedValue: T, initialValue: T): T { - if (typeof storedValue === 'object' && storedValue !== null) { + if ( + !Array.isArray(storedValue) && + typeof storedValue === 'object' && + storedValue !== null + ) { return { ...initialValue, ...storedValue } } return storedValue @@ -67,5 +71,7 @@ export function useSessionStorage( } }, [key, initialValue]) - return [JSON.parse(data) as T, setState] + const memoized = useMemo(() => JSON.parse(data) as T, [data]) + + return [memoized, setState] } -- 2.34.1 From 127c1fd8a63c3a38a9ecd1589fe8abcc91cbed38 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 5 Dec 2024 21:03:00 +0100 Subject: [PATCH 12/21] fix(category): use hierarchy links, visual indicator for link --- .../Filters/CategoryFilterPopup.tsx | 42 ++++--- src/components/ModForm.tsx | 26 ++-- src/pages/game.tsx | 115 ++++++++++++------ src/pages/mod/index.tsx | 34 +++--- 4 files changed, 134 insertions(+), 83 deletions(-) diff --git a/src/components/Filters/CategoryFilterPopup.tsx b/src/components/Filters/CategoryFilterPopup.tsx index 06f287e..8e6fcc8 100644 --- a/src/components/Filters/CategoryFilterPopup.tsx +++ b/src/components/Filters/CategoryFilterPopup.tsx @@ -9,44 +9,48 @@ import styles from './CategoryFilterPopup.module.scss' interface CategoryFilterPopupProps { categories: string[] setCategories: React.Dispatch> - heirarchies: string[] - setHeirarchies: React.Dispatch> + hierarchies: string[] + setHierarchies: React.Dispatch> handleClose: () => void - handleApply: () => void } export const CategoryFilterPopup = ({ categories, setCategories, - heirarchies, - setHeirarchies, - handleClose, - handleApply + hierarchies, + setHierarchies, + handleClose }: CategoryFilterPopupProps) => { + const [filterCategories, setFilterCategories] = useState(categories) + const [filterHierarchies, setFilterHierarchies] = useState(hierarchies) + const handleApply = () => { + setCategories(filterCategories) + setHierarchies(filterHierarchies) + } const [inputValue, setInputValue] = useState('') const handleInputChange = (e: React.ChangeEvent) => { setInputValue(e.target.value) } const handleSingleSelection = (category: string, isSelected: boolean) => { - let updatedCategories = [...categories] + let updatedCategories = [...filterCategories] if (isSelected) { updatedCategories.push(category) } else { updatedCategories = updatedCategories.filter((item) => item !== category) } - setCategories(updatedCategories) + setFilterCategories(updatedCategories) } const handleCombinationSelection = (path: string[], isSelected: boolean) => { const pathString = path.join(':') - let updatedHeirarchies = [...heirarchies] + let updatedHierarchies = [...filterHierarchies] if (isSelected) { - updatedHeirarchies.push(pathString) + updatedHierarchies.push(pathString) } else { - updatedHeirarchies = updatedHeirarchies.filter( + updatedHierarchies = updatedHierarchies.filter( (item) => item !== pathString ) } - setHeirarchies(updatedHeirarchies) + setFilterHierarchies(updatedHierarchies) } const handleAddNew = () => { if (inputValue) { @@ -55,9 +59,9 @@ export const CategoryFilterPopup = ({ .split('>') .map((s) => s.trim()) if (values.length > 1) { - setHeirarchies([...categories, values.join(':')]) + setFilterHierarchies([...filterHierarchies, values.join(':')]) } else { - setCategories([...categories, values[0]]) + setFilterCategories([...filterCategories, values[0]]) } setInputValue('') } @@ -157,8 +161,8 @@ export const CategoryFilterPopup = ({ path={[category.name]} handleSingleSelection={handleSingleSelection} handleCombinationSelection={handleCombinationSelection} - selectedSingles={categories} - selectedCombinations={heirarchies} + selectedSingles={filterCategories} + selectedCombinations={filterHierarchies} /> ))}
@@ -181,8 +185,8 @@ export const CategoryFilterPopup = ({ className='btn btnMain btnMainPopup' type='button' onPointerDown={() => { - setCategories([]) - setHeirarchies([]) + setFilterCategories([]) + setFilterHierarchies([]) }} > Reset diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index 9711ac4..56ec6b1 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -235,7 +235,7 @@ export const ModForm = ({ existingModData }: ModFormProps) => { } // Prepend com.degmods to avoid leaking categories to 3rd party client's search - // Add heirarchical namespaces labels + // Add hierarchical namespaces labels if (formState.LTags.length > 0) { for (let i = 0; i < formState.LTags.length; i++) { tags.push(['L', `com.degmods:${formState.LTags[i]}`]) @@ -946,12 +946,12 @@ export const CategoryAutocomplete = ({ const concatenatedValue = Array.from(uniqueValues) return concatenatedValue } - const getSelectedHeirarchy = (cats: Categories[]) => { - const heirarchies = cats.reduce( + const getSelectedhierarchy = (cats: Categories[]) => { + const hierarchies = cats.reduce( (prev, cat) => [...prev, cat.hierarchy.replace(/ > /g, ':')], [] ) - const concatenatedValue = Array.from(heirarchies) + const concatenatedValue = Array.from(hierarchies) return concatenatedValue } const handleReset = () => { @@ -989,7 +989,7 @@ export const CategoryAutocomplete = ({ setFormState((prevState) => ({ ...prevState, ['lTags']: getSelectedCategories(selectedCategories), - ['LTags']: getSelectedHeirarchy(selectedCategories) + ['LTags']: getSelectedhierarchy(selectedCategories) })) }, [selectedCategories, setFormState]) @@ -1134,16 +1134,20 @@ export const CategoryAutocomplete = ({ {LTags.length > 0 && (
{LTags.map((hierarchy) => { - const heirarchicalCategories = hierarchy.split(`:`) - const categories = heirarchicalCategories - .map((c, i) => - game ? ( + const hierarchicalCategories = hierarchy.split(`:`) + const categories = hierarchicalCategories + .map((c, i) => { + const partialHierarchy = hierarchicalCategories + .slice(0, i + 1) + .join(':') + + return game ? ( @@ -1154,7 +1158,7 @@ export const CategoryAutocomplete = ({ {capitalizeEachWord(c)}

) - ) + }) .reduce((prev, curr, i) => [ prev,
{ const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '') // Categories filter - const [categories, setCategories] = useState(searchParams.getAll('l') || []) - const [heirarchies, setHeirarchies] = useState(searchParams.getAll('h') || []) + const [categories, setCategories] = useSessionStorage('l', []) + const [hierarchies, setHierarchies] = useSessionStorage('h', []) const [showCategoryPopup, setShowCategoryPopup] = useState(false) + const linkedHierarchy = searchParams.get('h') + const isCategoryFilterActive = categories.length + hierarchies.length > 0 const handleSearch = () => { const value = searchTermRef.current?.value || '' // Access the input value from the ref @@ -134,12 +137,17 @@ export const GamePage = () => { '#t': [T_TAG_VALUE] } - if (categories.length) { - filter['#l'] = categories.map((l) => `com.degmods:${l}`) - } + // Linked category will override the filter + if (linkedHierarchy && linkedHierarchy !== '') { + filter['#L'] = [`com.degmods:${linkedHierarchy}`] + } else { + if (categories.length) { + filter['#l'] = categories.map((l) => `com.degmods:${l}`) + } - if (heirarchies.length) { - filter['#L'] = heirarchies.map((L) => `com.degmods:${L}`) + if (hierarchies.length) { + filter['#L'] = hierarchies.map((L) => `com.degmods:${L}`) + } } const subscription = ndk.subscribe(filter, { @@ -165,7 +173,7 @@ export const GamePage = () => { return () => { subscription.stop() } - }, [gameName, ndk, categories, heirarchies]) + }, [gameName, ndk, linkedHierarchy, categories, hierarchies]) if (!gameName) return null @@ -203,26 +211,73 @@ export const GamePage = () => {
-
- -
+ {linkedHierarchy.replace(/:/g, ' > ')} + + + + + ) : ( +
+ +
+ )}
@@ -244,29 +299,11 @@ export const GamePage = () => { { setShowCategoryPopup(false) }} - handleApply={() => { - searchParams.delete('l') - searchParams.delete('h') - categories.forEach((l) => { - if (l) { - searchParams.delete('h') - searchParams.append('l', l) - } - }) - heirarchies.forEach((h) => { - if (h) { - searchParams.append('h', h) - } - }) - setSearchParams(searchParams, { - replace: true - }) - }} /> )} diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx index 587beb2..dc94295 100644 --- a/src/pages/mod/index.tsx +++ b/src/pages/mod/index.tsx @@ -543,20 +543,26 @@ const Body = ({ {LTags.length > 0 && (
{LTags.map((hierarchy) => { - const heirarchicalCategories = hierarchy.split(`:`) - const categories = heirarchicalCategories - .map((c: string) => ( - -

{capitalizeEachWord(c)}

-
- )) + const hierarchicalCategories = hierarchy.split(`:`) + const categories = hierarchicalCategories + .map((c, i) => { + const partialHierarchy = hierarchicalCategories + .slice(0, i + 1) + .join(':') + + return ( + +

{capitalizeEachWord(c)}

+
+ ) + }) .reduce((prev, curr) => [ prev,
-- 2.34.1 From 535aabe4a31d82480dc31db6ceb4099d3a9e265e Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 11 Dec 2024 14:02:10 +0100 Subject: [PATCH 13/21] build(audit): update packages --- package-lock.json | 265 +++++++++++++++++++++++++++++----------------- package.json | 2 +- 2 files changed, 170 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19b9ff3..5368607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@tiptap/react": "2.9.1", "@tiptap/starter-kit": "2.9.1", "@types/react-helmet": "^6.1.11", - "axios": "1.7.3", + "axios": "^1.7.9", "bech32": "2.0.0", "buffer": "6.0.3", "date-fns": "3.6.0", @@ -1206,208 +1206,266 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", - "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", + "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", - "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", + "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", - "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", + "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", - "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", + "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", + "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", + "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", - "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", + "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", - "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", + "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", - "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", + "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", - "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", + "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", + "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", - "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", + "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", - "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", + "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", - "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", + "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", - "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", + "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz", - "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", + "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", - "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", + "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", - "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", + "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", - "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", + "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1993,10 +2051,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/file-saver": { "version": "2.0.7", @@ -2434,9 +2493,10 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", - "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2719,10 +2779,11 @@ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4359,10 +4420,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -4377,9 +4439,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "dev": true, "funding": [ { @@ -4395,10 +4457,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4912,12 +4975,13 @@ } }, "node_modules/rollup": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", - "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", + "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -4927,22 +4991,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.1", - "@rollup/rollup-android-arm64": "4.18.1", - "@rollup/rollup-darwin-arm64": "4.18.1", - "@rollup/rollup-darwin-x64": "4.18.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", - "@rollup/rollup-linux-arm-musleabihf": "4.18.1", - "@rollup/rollup-linux-arm64-gnu": "4.18.1", - "@rollup/rollup-linux-arm64-musl": "4.18.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", - "@rollup/rollup-linux-riscv64-gnu": "4.18.1", - "@rollup/rollup-linux-s390x-gnu": "4.18.1", - "@rollup/rollup-linux-x64-gnu": "4.18.1", - "@rollup/rollup-linux-x64-musl": "4.18.1", - "@rollup/rollup-win32-arm64-msvc": "4.18.1", - "@rollup/rollup-win32-ia32-msvc": "4.18.1", - "@rollup/rollup-win32-x64-msvc": "4.18.1", + "@rollup/rollup-android-arm-eabi": "4.28.1", + "@rollup/rollup-android-arm64": "4.28.1", + "@rollup/rollup-darwin-arm64": "4.28.1", + "@rollup/rollup-darwin-x64": "4.28.1", + "@rollup/rollup-freebsd-arm64": "4.28.1", + "@rollup/rollup-freebsd-x64": "4.28.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", + "@rollup/rollup-linux-arm-musleabihf": "4.28.1", + "@rollup/rollup-linux-arm64-gnu": "4.28.1", + "@rollup/rollup-linux-arm64-musl": "4.28.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", + "@rollup/rollup-linux-riscv64-gnu": "4.28.1", + "@rollup/rollup-linux-s390x-gnu": "4.28.1", + "@rollup/rollup-linux-x64-gnu": "4.28.1", + "@rollup/rollup-linux-x64-musl": "4.28.1", + "@rollup/rollup-win32-arm64-msvc": "4.28.1", + "@rollup/rollup-win32-ia32-msvc": "4.28.1", + "@rollup/rollup-win32-x64-msvc": "4.28.1", "fsevents": "~2.3.2" } }, @@ -5042,10 +5109,11 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5397,14 +5465,15 @@ "dev": true }, "node_modules/vite": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", - "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -5423,6 +5492,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -5440,6 +5510,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, diff --git a/package.json b/package.json index b356c8a..a99abbb 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@tiptap/react": "2.9.1", "@tiptap/starter-kit": "2.9.1", "@types/react-helmet": "^6.1.11", - "axios": "1.7.3", + "axios": "^1.7.9", "bech32": "2.0.0", "buffer": "6.0.3", "date-fns": "3.6.0", -- 2.34.1 From f7f8778707ac92205d026e4da5cc32129bc9dac2 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 11 Dec 2024 14:03:33 +0100 Subject: [PATCH 14/21] feat(category): user hierarchy, fix filter --- src/assets/categories/categories.json | 5 +- .../Filters/CategoryFilterPopup.tsx | 150 +++++++++++++++--- src/components/ModForm.tsx | 68 +++----- src/pages/game.tsx | 62 +++++--- src/styles/styles.css | 10 +- src/utils/category.ts | 67 ++++++++ 6 files changed, 263 insertions(+), 99 deletions(-) diff --git a/src/assets/categories/categories.json b/src/assets/categories/categories.json index 2948280..eaa4079 100644 --- a/src/assets/categories/categories.json +++ b/src/assets/categories/categories.json @@ -3,7 +3,10 @@ "name": "audio", "sub": [ { "name": "music", "sub": ["background", "ambient"] }, - { "name": "sound effects", "sub": ["footsteps", "weapons"] }, + { + "name": "sound effects", + "sub": ["footsteps", "weapons"] + }, "voice" ] }, diff --git a/src/components/Filters/CategoryFilterPopup.tsx b/src/components/Filters/CategoryFilterPopup.tsx index 8e6fcc8..252f73d 100644 --- a/src/components/Filters/CategoryFilterPopup.tsx +++ b/src/components/Filters/CategoryFilterPopup.tsx @@ -1,10 +1,14 @@ import React, { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { Category } from 'types' -import categoriesData from './../../assets/categories/categories.json' -import { capitalizeEachWord } from 'utils' - +import { + addToUserCategories, + capitalizeEachWord, + deleteFromUserCategories +} from 'utils' +import { useLocalStorage } from 'hooks' import styles from './CategoryFilterPopup.module.scss' +import categoriesData from './../../assets/categories/categories.json' interface CategoryFilterPopupProps { categories: string[] @@ -21,6 +25,9 @@ export const CategoryFilterPopup = ({ setHierarchies, handleClose }: CategoryFilterPopupProps) => { + const [userHierarchies, setUserHierarchies] = useLocalStorage< + (string | Category)[] + >('user-hierarchies', []) const [filterCategories, setFilterCategories] = useState(categories) const [filterHierarchies, setFilterHierarchies] = useState(hierarchies) const handleApply = () => { @@ -54,15 +61,28 @@ export const CategoryFilterPopup = ({ } const handleAddNew = () => { if (inputValue) { - const values = inputValue + const value = inputValue.toLowerCase() + const values = value .trim() .split('>') .map((s) => s.trim()) - if (values.length > 1) { - setFilterHierarchies([...filterHierarchies, values.join(':')]) - } else { - setFilterCategories([...filterCategories, values[0]]) - } + + setUserHierarchies((prev) => { + addToUserCategories(prev, value) + return [...prev] + }) + + const path = values.join(':') + + // Add new hierarchy to current selection and active selection + setFilterHierarchies((prev) => { + prev.push(path) + return [...prev] + }) + setHierarchies((prev) => { + prev.push(path) + return [...prev] + }) setInputValue('') } } @@ -104,23 +124,67 @@ export const CategoryFilterPopup = ({
- {true && ( -
-
+ )}
- Search for "{inputValue}" category -
- {(categoriesData as Category[]).map((category) => ( + {(categoriesData as Category[]).map((category, i) => ( void } const CategoryCheckbox: React.FC = ({ @@ -231,7 +300,8 @@ const CategoryCheckbox: React.FC = ({ handleCombinationSelection, selectedSingles, selectedCombinations, - indentLevel = 0 + indentLevel = 0, + handleRemove }) => { const name = typeof category === 'string' ? category : category.name const isMatching = path @@ -330,6 +400,24 @@ const CategoryCheckbox: React.FC = ({ checked={isSingleChecked} onChange={handleSingleChange} /> + {typeof handleRemove === 'function' && ( + + )}
)} @@ -350,6 +438,11 @@ const CategoryCheckbox: React.FC = ({ selectedSingles={selectedSingles} selectedCombinations={selectedCombinations} indentLevel={indentLevel + 1} + {...(typeof handleRemove === 'function' + ? { + handleRemove + } + : {})} /> ) } else { @@ -364,6 +457,11 @@ const CategoryCheckbox: React.FC = ({ selectedSingles={selectedSingles} selectedCombinations={selectedCombinations} indentLevel={indentLevel + 1} + {...(typeof handleRemove === 'function' + ? { + handleRemove + } + : {})} /> ) } diff --git a/src/components/ModForm.tsx b/src/components/ModForm.tsx index 56ec6b1..efcf475 100644 --- a/src/components/ModForm.tsx +++ b/src/components/ModForm.tsx @@ -1,7 +1,6 @@ import _ from 'lodash' import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools' import React, { - CSSProperties, Fragment, useCallback, useEffect, @@ -11,7 +10,7 @@ import React, { } from 'react' import { Link, useLocation, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' -import { VariableSizeList, FixedSizeList } from 'react-window' +import { FixedSizeList } from 'react-window' import { v4 as uuidv4 } from 'uuid' import { T_TAG_VALUE } from '../constants' import { useAppSelector, useGames, useNDKContext } from '../hooks' @@ -73,9 +72,10 @@ export const ModForm = ({ existingModData }: ModFormProps) => { useEffect(() => { if (location.pathname === appRoutes.submitMod) { + // Only trigger when the pathname changes to submit-mod setFormState(initializeFormState()) } - }, [location.pathname]) // Only trigger when the pathname changes to submit-mod + }, [location.pathname]) useEffect(() => { if (existingModData) { @@ -974,7 +974,7 @@ export const CategoryAutocomplete = ({ } const handleAddNew = () => { if (inputValue) { - const value = inputValue.trim() + const value = inputValue.trim().toLowerCase() const newOption: Categories = { name: value, hierarchy: value, @@ -993,44 +993,11 @@ export const CategoryAutocomplete = ({ })) }, [selectedCategories, setFormState]) - const listRef = useRef(null) - const rowHeights = useRef<{ [index: number]: number }>({}) - const setRowHeight = (index: number, size: number) => { - rowHeights.current = { ...rowHeights.current, [index]: size } - if (listRef.current) { - listRef.current.resetAfterIndex(index) - } - } - const getRowHeight = (index: number) => { - return (rowHeights.current[index] || 35) + 8 - } - - const Row = ({ index, style }: { index: number; style: CSSProperties }) => { - const rowRef = useRef(null) - - useEffect(() => { - const rowElement = rowRef.current - if (!rowElement) return - const updateHeight = () => { - const height = Math.max(rowElement.scrollHeight, 35) - setRowHeight(index, height) - } - const observer = new ResizeObserver(() => { - updateHeight() - }) - observer.observe(rowElement) - updateHeight() - return () => { - observer.disconnect() - } - }, [index]) - + const Row = ({ index }: { index: number }) => { if (!filteredOptions) return null return (
handleSelect(filteredOptions[index])} > @@ -1039,6 +1006,7 @@ export const CategoryAutocomplete = ({ (cat) => cat.hierarchy === filteredOptions[index].hierarchy ) && ( -
+
{filteredOptions && filteredOptions.length > 0 ? ( - - {Row} - + filteredOptions.map((c, i) => ) ) : (
Add "{inputValue}" -
+ ) : ( +
- - - - -
+ + + + +
+ )}
{(categoriesData as Category[]).map((category, i) => ( { @@ -26,16 +26,6 @@ export const getCategories = () => { return flattenCategories(categoriesData) } -export const buildCategories = (input: string[]) => { - const categories: (string | Category)[] = [] - - input.forEach((cat) => { - addToUserCategories(categories, cat) - }) - - return categories -} - export const addToUserCategories = ( categories: (string | Category)[], input: string -- 2.34.1 From bac48a448611fd767c258e6ee9855cbb17e60418 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 11 Dec 2024 16:26:16 +0100 Subject: [PATCH 16/21] feat(category): indeterminate state, parent marking --- .../Filters/CategoryFilterPopup.tsx | 57 ++++++++++--------- src/styles/styles.css | 9 +++ 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/components/Filters/CategoryFilterPopup.tsx b/src/components/Filters/CategoryFilterPopup.tsx index d7c3840..a3da21e 100644 --- a/src/components/Filters/CategoryFilterPopup.tsx +++ b/src/components/Filters/CategoryFilterPopup.tsx @@ -348,28 +348,35 @@ const CategoryCheckbox: React.FC = ({ const pathString = path.join(':') setIsSingleChecked(selectedSingles.includes(name)) setIsCombinationChecked(selectedCombinations.includes(pathString)) - - const childPaths = - category.sub && Array.isArray(category.sub) - ? category.sub.map((sub) => - typeof sub === 'string' - ? [...path, sub].join(':') - : [...path, sub.name].join(':') - ) - : [] + // Recursive function to gather all descendant paths + const collectChildPaths = ( + category: string | Category, + basePath: string[] + ) => { + if (!category.sub || !Array.isArray(category.sub)) { + return [] + } + let paths: string[] = [] + for (const sub of category.sub) { + const subPath = + typeof sub === 'string' + ? [...basePath, sub].join(':') + : [...basePath, sub.name].join(':') + paths.push(subPath) + if (typeof sub === 'object') { + paths = paths.concat(collectChildPaths(sub, [...basePath, sub.name])) + } + } + return paths + } + const childPaths = collectChildPaths(category, path) const anyChildCombinationSelected = childPaths.some((childPath) => selectedCombinations.includes(childPath) ) - - if ( - anyChildCombinationSelected && - !selectedCombinations.includes(pathString) - ) { - setIsIndeterminate(true) - } else { - setIsIndeterminate(false) - } - }, [selectedSingles, selectedCombinations, path, name, category.sub]) + setIsIndeterminate( + anyChildCombinationSelected && !selectedCombinations.includes(pathString) + ) + }, [category, name, path, selectedCombinations, selectedSingles]) const handleSingleChange = () => { setIsSingleChecked(!isSingleChecked) @@ -407,17 +414,13 @@ const CategoryCheckbox: React.FC = ({ input.indeterminate = isIndeterminate } }} - className='CheckboxMain' + className={`CheckboxMain ${ + isIndeterminate ? 'CheckboxIndeterminate' : '' + }`} checked={isCombinationChecked} onChange={handleCombinationChange} /> -
Date: Thu, 12 Dec 2024 17:29:48 +0100 Subject: [PATCH 21/21] refactor: remove unused variable --- src/components/CategoryAutocomplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CategoryAutocomplete.tsx b/src/components/CategoryAutocomplete.tsx index dff35c5..4c2ea50 100644 --- a/src/components/CategoryAutocomplete.tsx +++ b/src/components/CategoryAutocomplete.tsx @@ -1,5 +1,5 @@ import { useLocalStorage } from 'hooks' -import { useMemo, useState, useEffect } from 'react' +import { useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { getGamePageRoute } from 'routes' import { ModFormState, Categories, Category } from 'types' -- 2.34.1