mirror of
https://github.com/plebbit/seedit.git
synced 2026-02-15 00:11:10 -05:00
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "Seedit",
|
||||
"name": "Seedit",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
||||
@@ -15,7 +15,7 @@ import Profile from './views/profile';
|
||||
import Settings from './views/settings';
|
||||
import SubmitPage from './views/submit-page';
|
||||
import Subplebbit from './views/subplebbit';
|
||||
import SubplebbitSettings from './views/subplebbit/subplebbit-settings';
|
||||
import SubplebbitSettings from './views/subplebbit-settings';
|
||||
import Subplebbits from './views/subplebbits';
|
||||
import AccountBar from './components/account-bar/';
|
||||
import ChallengeModal from './components/challenge-modal';
|
||||
|
||||
@@ -136,6 +136,7 @@ const AuthorHeaderTabs = () => {
|
||||
!isInProfileCommentsView &&
|
||||
!isInProfileSubmittedView &&
|
||||
!isInAuthorCommentsView &&
|
||||
!isInProfileHiddenView &&
|
||||
!isInAuthorSubmittedView
|
||||
? styles.selected
|
||||
: styles.choice;
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
.newerPostsButton {
|
||||
position: fixed;
|
||||
left: 10%;
|
||||
border-radius: 25px;
|
||||
background-color: green;
|
||||
color: white;
|
||||
z-index: 5;
|
||||
transition: top 0.3s ease-in-out;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.resetButton {
|
||||
display: inline-block;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.resetButton:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hideButton {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
background-image: url("/public/assets/buttons/close-x-button.png");
|
||||
background-size: cover;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.hideButtonWrapper {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.hideButton:hover {
|
||||
opacity: 0.8;
|
||||
padding: 0px 3px;
|
||||
}
|
||||
@@ -38,11 +38,13 @@ const NewerPostsButton = ({ reset, subplebbitAddressesWithNewerPosts }: NewerPos
|
||||
|
||||
return (
|
||||
<div className={`${styles.newerPostsButton} ${!hide && styles.show} ${hide && styles.hide}`} style={{ top: `${buttonPosition}px` }}>
|
||||
<span className={styles.resetButton} onClick={handleNewerPostsButtonClick}>
|
||||
Newer Posts
|
||||
</span>
|
||||
<button className={styles.resetButton} onClick={handleNewerPostsButtonClick}>
|
||||
↑ new posts
|
||||
</button>
|
||||
<span className={styles.hideButtonWrapper}>
|
||||
<span className={styles.hideButton} onClick={() => setHideButton(true)} />
|
||||
<button className={styles.hideButton} onClick={() => setHideButton(true)}>
|
||||
x
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -122,9 +122,7 @@
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
max-width: 19em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ const Post = ({ index, post = {} }: PostProps) => {
|
||||
<span className={styles.domain}>
|
||||
(
|
||||
<a href={link} target='_blank' rel='noopener noreferrer'>
|
||||
{linkUrl}
|
||||
{linkUrl.length > 25 ? linkUrl.slice(0, 25) + '...' : linkUrl}
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
|
||||
@@ -58,13 +58,13 @@ a {
|
||||
|
||||
.descriptionTitle {
|
||||
padding-top: 5px;
|
||||
color: var(--text-markdown);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 5px 0;
|
||||
word-wrap: break-word;
|
||||
color: var(--text-markdown);
|
||||
color: var(--text);
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ a {
|
||||
.rules {
|
||||
padding-bottom: 5px;
|
||||
padding-top: 7px;
|
||||
color: var(--text-markdown);
|
||||
color: var(--text);
|
||||
line-height: 15px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -67,27 +67,74 @@ const SortDropdown = ({ onSortChange }: SortDropdownProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
const Profile = () => {
|
||||
const { t } = useTranslation();
|
||||
const account = useAccount();
|
||||
const location = useLocation();
|
||||
const params = useParams();
|
||||
let { accountComments } = useAccountComments();
|
||||
accountComments = [...accountComments].reverse();
|
||||
const { accountVotes } = useAccountVotes();
|
||||
const isMobile = useWindowWidth() < 640;
|
||||
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const isInProfileUpvotedView = isProfileUpvotedView(location.pathname);
|
||||
const isInProfileDownvotedView = isProfileDownvotedView(location.pathname);
|
||||
const isInProfileHiddenView = isProfileHiddenView(location.pathname);
|
||||
const isInCommentsView = isProfileCommentsView(location.pathname);
|
||||
const isInSubmittedView = isProfileSubmittedView(location.pathname);
|
||||
const isMobile = useWindowWidth() < 640;
|
||||
|
||||
// get comments for upvoted/downvoted/comments/submitted pages
|
||||
useEffect(() => {
|
||||
if (isInProfileUpvotedView) setActiveTab('upvoted');
|
||||
else if (isInProfileDownvotedView) setActiveTab('downvoted');
|
||||
else if (isInProfileHiddenView) setActiveTab('hidden');
|
||||
else if (isInCommentsView) setActiveTab('comments');
|
||||
else if (isInSubmittedView) setActiveTab('submitted');
|
||||
else setActiveTab('overview');
|
||||
|
||||
setCurrentPage(1); // Reset page when changing tabs
|
||||
}, [isInProfileUpvotedView, isInProfileDownvotedView, isInProfileHiddenView, isInCommentsView, isInSubmittedView]);
|
||||
|
||||
const { accountComments } = useAccountComments();
|
||||
const { accountVotes } = useAccountVotes();
|
||||
|
||||
const postComments = useMemo(() => accountComments?.filter((comment) => !comment.parentCid) || [], [accountComments]);
|
||||
const replyComments = useMemo(() => accountComments?.filter((comment) => comment.parentCid) || [], [accountComments]);
|
||||
const upvotedCommentCids = useMemo(() => accountVotes?.filter((vote) => vote.vote === 1).map((vote) => vote.commentCid) || [], [accountVotes]);
|
||||
const downvotedCommentCids = useMemo(() => accountVotes?.filter((vote) => vote.vote === -1).map((vote) => vote.commentCid) || [], [accountVotes]);
|
||||
const hiddenCommentCids = useMemo(() => Object.keys(account?.blockedCids ?? {}), [account?.blockedCids]);
|
||||
|
||||
const upvotedCommentCids = useMemo(() => {
|
||||
const allUpvotedCids = accountVotes?.filter((vote) => vote.vote === 1).map((vote) => vote.commentCid) || [];
|
||||
return allUpvotedCids.slice(0, currentPage * pageSize);
|
||||
}, [accountVotes, currentPage]);
|
||||
|
||||
const downvotedCommentCids = useMemo(() => {
|
||||
const allDownvotedCids = accountVotes?.filter((vote) => vote.vote === -1).map((vote) => vote.commentCid) || [];
|
||||
return allDownvotedCids.slice(0, currentPage * pageSize);
|
||||
}, [accountVotes, currentPage]);
|
||||
|
||||
const hiddenCommentCids = useMemo(() => {
|
||||
const allHiddenCids = Object.keys(account?.blockedCids ?? {});
|
||||
return allHiddenCids.slice(0, currentPage * pageSize);
|
||||
}, [account?.blockedCids, currentPage]);
|
||||
|
||||
const { hasMoreUpvoted, hasMoreDownvoted, hasMoreHidden } = useMemo(() => {
|
||||
const allUpvotedCids = accountVotes?.filter((vote) => vote.vote === 1).map((vote) => vote.commentCid) || [];
|
||||
const allDownvotedCids = accountVotes?.filter((vote) => vote.vote === -1).map((vote) => vote.commentCid) || [];
|
||||
const allHiddenCids = Object.keys(account?.blockedCids ?? {});
|
||||
|
||||
return {
|
||||
hasMoreUpvoted: currentPage * pageSize < allUpvotedCids.length,
|
||||
hasMoreDownvoted: currentPage * pageSize < allDownvotedCids.length,
|
||||
hasMoreHidden: currentPage * pageSize < allHiddenCids.length,
|
||||
};
|
||||
}, [accountVotes, account?.blockedCids, currentPage]);
|
||||
|
||||
const hasMore = useMemo(() => {
|
||||
if (isInProfileUpvotedView) return hasMoreUpvoted;
|
||||
if (isInProfileDownvotedView) return hasMoreDownvoted;
|
||||
if (isInProfileHiddenView) return hasMoreHidden;
|
||||
return false;
|
||||
}, [hasMoreUpvoted, hasMoreDownvoted, hasMoreHidden, isInProfileUpvotedView, isInProfileDownvotedView, isInProfileHiddenView]);
|
||||
|
||||
const { comments: upvotedComments } = useComments({ commentCids: upvotedCommentCids });
|
||||
const { comments: downvotedComments } = useComments({ commentCids: downvotedCommentCids });
|
||||
@@ -96,45 +143,51 @@ const Profile = () => {
|
||||
const [sortType, setSortType] = useState('new');
|
||||
const handleSortChange = (newSortType: string) => {
|
||||
setSortType(newSortType);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const comments = useMemo(() => {
|
||||
if (isInProfileUpvotedView) {
|
||||
return upvotedComments;
|
||||
} else if (isInProfileDownvotedView) {
|
||||
return downvotedComments;
|
||||
} else if (isInCommentsView) {
|
||||
return replyComments;
|
||||
} else if (isInSubmittedView) {
|
||||
return postComments;
|
||||
} else if (isInProfileHiddenView) {
|
||||
return hiddenComments;
|
||||
} else {
|
||||
return accountComments;
|
||||
let selectedComments;
|
||||
switch (activeTab) {
|
||||
case 'upvoted':
|
||||
selectedComments = upvotedComments;
|
||||
break;
|
||||
case 'downvoted':
|
||||
selectedComments = downvotedComments;
|
||||
break;
|
||||
case 'hidden':
|
||||
selectedComments = hiddenComments;
|
||||
break;
|
||||
case 'comments':
|
||||
selectedComments = replyComments;
|
||||
break;
|
||||
case 'submitted':
|
||||
selectedComments = postComments;
|
||||
break;
|
||||
case 'overview':
|
||||
default:
|
||||
selectedComments = [...postComments, ...replyComments];
|
||||
}
|
||||
}, [
|
||||
isInProfileUpvotedView,
|
||||
isInProfileDownvotedView,
|
||||
isInProfileHiddenView,
|
||||
isInCommentsView,
|
||||
isInSubmittedView,
|
||||
upvotedComments,
|
||||
downvotedComments,
|
||||
replyComments,
|
||||
postComments,
|
||||
hiddenComments,
|
||||
accountComments,
|
||||
]);
|
||||
|
||||
const virtuosoData = useMemo(() => {
|
||||
let sortedData = [...comments];
|
||||
if (sortType === 'new') {
|
||||
sortedData.sort((a, b) => b!.timestamp - a!.timestamp);
|
||||
} else {
|
||||
sortedData.sort((a, b) => a!.timestamp - b!.timestamp);
|
||||
}
|
||||
return sortedData;
|
||||
}, [sortType, comments]);
|
||||
// Sort comments
|
||||
selectedComments.sort((a, b) => (sortType === 'new' ? b!.timestamp - a!.timestamp : a!.timestamp - b!.timestamp));
|
||||
|
||||
return selectedComments;
|
||||
}, [activeTab, upvotedComments, downvotedComments, hiddenComments, replyComments, postComments, sortType]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
console.log('LoadMore called, current page:', currentPage);
|
||||
setCurrentPage((prevPage) => {
|
||||
const newPage = prevPage + 1;
|
||||
console.log('Setting new page:', newPage);
|
||||
return newPage;
|
||||
});
|
||||
}, [currentPage]);
|
||||
|
||||
const profileTitle = account?.author?.displayName ? `${account?.author?.displayName} (u/${account?.author?.shortAddress})` : `u/${account?.author?.shortAddress}`;
|
||||
useEffect(() => {
|
||||
document.title = profileTitle + ' - Seedit';
|
||||
}, [t, profileTitle]);
|
||||
|
||||
// save last virtuoso state on each scroll
|
||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||
@@ -150,28 +203,23 @@ const Profile = () => {
|
||||
return () => window.removeEventListener('scroll', setLastVirtuosoState);
|
||||
}, [account?.shortAddress, params.sortType]);
|
||||
|
||||
const profileTitle = account?.author?.displayName ? `${account?.author?.displayName} (u/${account?.author?.shortAddress})` : `u/${account?.author?.shortAddress}`;
|
||||
useEffect(() => {
|
||||
document.title = profileTitle + ' - Seedit';
|
||||
}, [t, profileTitle]);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={isMobile ? styles.sidebarMobile : styles.sidebarDesktop}>
|
||||
<AuthorSidebar />
|
||||
</div>
|
||||
<SortDropdown onSortChange={handleSortChange} />
|
||||
{account && !accountComments.length ? (
|
||||
t('no_posts')
|
||||
{account && comments.length === 0 ? (
|
||||
<div>{t('no_posts')}</div>
|
||||
) : (
|
||||
<Virtuoso
|
||||
increaseViewportBy={{ bottom: 1200, top: 600 }}
|
||||
totalCount={accountComments?.length || 0}
|
||||
data={virtuosoData}
|
||||
itemContent={(index, post) => {
|
||||
const isReply = post?.parentCid;
|
||||
return !isReply ? <Post index={index} post={post} /> : <Reply index={index} isSingleReply={true} reply={post} />;
|
||||
}}
|
||||
data={comments}
|
||||
totalCount={comments.length}
|
||||
itemContent={(index, post) =>
|
||||
post?.parentCid ? <Reply key={post?.cid} index={index} isSingleReply={true} reply={post} /> : <Post key={post?.cid} index={index} post={post} />
|
||||
}
|
||||
endReached={hasMore ? loadMore : undefined}
|
||||
useWindowScroll={true}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
|
||||
@@ -102,6 +102,7 @@ const AccountSettings = () => {
|
||||
await importAccount(fileContent);
|
||||
setSwitchToLastAccount(true);
|
||||
alert(`Imported ${newAccount.account?.name}`);
|
||||
window.location.reload();
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} catch (error) {
|
||||
|
||||
@@ -9,17 +9,17 @@ import {
|
||||
useSubplebbit,
|
||||
usePublishSubplebbitEdit,
|
||||
} from '@plebbit/plebbit-react-hooks';
|
||||
import { Roles } from '../../../lib/utils/user-utils';
|
||||
import { Roles } from '../../lib/utils/user-utils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { create } from 'zustand';
|
||||
import styles from './subplebbit-settings.module.css';
|
||||
import { isValidURL } from '../../../lib/utils/url-utils';
|
||||
import { isCreateSubplebbitView, isSubplebbitSettingsView } from '../../../lib/utils/view-utils';
|
||||
import useChallengesOptions from '../../../hooks/use-challenges-options';
|
||||
import useChallengeSettings from '../../../hooks/use-challenge-settings';
|
||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||
import Markdown from '../../../components/markdown';
|
||||
import Sidebar from '../../../components/sidebar';
|
||||
import { isValidURL } from '../../lib/utils/url-utils';
|
||||
import { isCreateSubplebbitView, isSubplebbitSettingsView } from '../../lib/utils/view-utils';
|
||||
import useChallengesOptions from '../../hooks/use-challenges-options';
|
||||
import useChallengeSettings from '../../hooks/use-challenge-settings';
|
||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||
import Markdown from '../../components/markdown';
|
||||
import Sidebar from '../../components/sidebar';
|
||||
import _ from 'lodash';
|
||||
|
||||
type SubplebbitSettingsState = {
|
||||
@@ -315,7 +315,7 @@ const Moderators = ({ isReadOnly = false }: { isReadOnly?: boolean }) => {
|
||||
</button>
|
||||
)}
|
||||
{roles &&
|
||||
Object.entries(roles).map(([address, role], index) => (
|
||||
Object.entries(roles)?.map(([address, role], index) => (
|
||||
<div className={`${styles.moderator} ${index === 0 && styles.firstModerator}`} key={index}>
|
||||
{t('moderator')} #{index + 1}
|
||||
{!isReadOnly && <span className={styles.deleteButton} title='delete moderator' onClick={() => (isReadOnly ? {} : handleDeleteModerator(address))} />}
|
||||
@@ -399,7 +399,7 @@ const ChallengeSettings = ({ challenge, index, isReadOnly, setSubplebbitSettings
|
||||
|
||||
const handleOptionChange = (optionName: string, newValue: string) => {
|
||||
const updatedOptions = { ...options, [optionName]: newValue };
|
||||
const updatedChallenges = settings.challenges.map((ch: any, idx: number) => (idx === index ? { ...ch, options: updatedOptions } : ch));
|
||||
const updatedChallenges = settings.challenges?.map((ch: any, idx: number) => (idx === index ? { ...ch, options: updatedOptions } : ch));
|
||||
setSubplebbitSettingsStore({ settings: { ...settings, challenges: updatedChallenges } });
|
||||
};
|
||||
|
||||
@@ -431,9 +431,9 @@ const ChallengeSettings = ({ challenge, index, isReadOnly, setSubplebbitSettings
|
||||
};
|
||||
|
||||
const handleExcludeChange = (excludeIndex: number, type: keyof Exclude | 'not post' | 'not reply' | 'not vote', value: any) => {
|
||||
const updatedChallenges = settings.challenges.map((ch: any, idx: number) => {
|
||||
const updatedChallenges = settings.challenges?.map((ch: any, idx: number) => {
|
||||
if (idx === index) {
|
||||
const updatedExclude = ch.exclude.map((ex: any, exIdx: number) => {
|
||||
const updatedExclude = ch.exclude?.map((ex: any, exIdx: number) => {
|
||||
if (exIdx === excludeIndex) {
|
||||
let newEx = { ...ex };
|
||||
|
||||
@@ -483,7 +483,7 @@ const ChallengeSettings = ({ challenge, index, isReadOnly, setSubplebbitSettings
|
||||
const handleExcludeAddress = (excludeIndex: number, value: string) => {
|
||||
const addresses = value
|
||||
.split(',')
|
||||
.map((addr) => addr.trim())
|
||||
?.map((addr) => addr.trim())
|
||||
.filter((addr) => addr !== '');
|
||||
handleExcludeChange(excludeIndex, 'address', addresses);
|
||||
};
|
||||
@@ -498,7 +498,7 @@ const ChallengeSettings = ({ challenge, index, isReadOnly, setSubplebbitSettings
|
||||
) : (
|
||||
<div className={styles.challengeDescription}>{challengeSettings?.description}</div>
|
||||
)}
|
||||
{challengeSettings?.optionInputs.map((setting: OptionInput) => (
|
||||
{challengeSettings?.optionInputs?.map((setting: OptionInput) => (
|
||||
<div key={setting?.option} className={styles.challengeOption}>
|
||||
<div className={styles.challengeOptionLabel}>{setting?.label}</div>
|
||||
<div className={styles.challengeOptionDescription}>
|
||||
@@ -594,7 +594,7 @@ const ChallengeSettings = ({ challenge, index, isReadOnly, setSubplebbitSettings
|
||||
<div className={styles.challengeOption}>
|
||||
User's role
|
||||
<div className={styles.challengeOptionDescription}>Is any of the following:</div>
|
||||
{rolesToExclude.map((role) =>
|
||||
{rolesToExclude?.map((role) =>
|
||||
isReadOnly && !exclude?.role?.includes(role) ? null : (
|
||||
<div key={role}>
|
||||
{isReadOnly ? (
|
||||
@@ -619,7 +619,7 @@ const ChallengeSettings = ({ challenge, index, isReadOnly, setSubplebbitSettings
|
||||
<div className={styles.challengeOption}>
|
||||
User's action
|
||||
<div className={styles.challengeOptionDescription}>Is all of the following:</div>
|
||||
{actionsToExclude.map((action) =>
|
||||
{actionsToExclude?.map((action) =>
|
||||
isReadOnly && !exclude?.[action] ? null : (
|
||||
<div key={action}>
|
||||
{isReadOnly ? (
|
||||
@@ -638,7 +638,7 @@ const ChallengeSettings = ({ challenge, index, isReadOnly, setSubplebbitSettings
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{nonActionsToExclude.map((nonAction) =>
|
||||
{nonActionsToExclude?.map((nonAction) =>
|
||||
isReadOnly && exclude?.[nonAction.replace('not ', '')] !== null ? null : (
|
||||
<div key={nonAction}>
|
||||
{isReadOnly ? (
|
||||
@@ -717,7 +717,7 @@ const Challenges = ({ isReadOnly, readOnlyChallenges }: { isReadOnly: boolean; r
|
||||
const { t } = useTranslation();
|
||||
const { settings, setSubplebbitSettingsStore } = useSubplebbitSettingsStore();
|
||||
const challenges = settings?.challenges || readOnlyChallenges || [];
|
||||
const [showSettings, setShowSettings] = useState<boolean[]>(challenges.map(() => false));
|
||||
const [showSettings, setShowSettings] = useState<boolean[]>(challenges?.map(() => false));
|
||||
const challengeOptions = useChallengesOptions();
|
||||
|
||||
const location = useLocation();
|
||||
@@ -764,7 +764,7 @@ const Challenges = ({ isReadOnly, readOnlyChallenges }: { isReadOnly: boolean; r
|
||||
</button>
|
||||
)}
|
||||
{challenges.length === 0 && !isInCreateSubplebbitView && <span className={styles.noChallengeWarning}>{t('warning_spam')}</span>}
|
||||
{challenges.map((challenge: any, index: number) => (
|
||||
{challenges?.map((challenge: any, index: number) => (
|
||||
<div key={index} className={styles.challenge}>
|
||||
Challenge #{index + 1}
|
||||
{!isReadOnly && <span className={styles.deleteButton} title='delete challenge' onClick={() => (isReadOnly ? {} : handleDeleteChallenge(index))} />}
|
||||
@@ -773,7 +773,7 @@ const Challenges = ({ isReadOnly, readOnlyChallenges }: { isReadOnly: boolean; r
|
||||
<span className={styles.readOnlyChallenge}>{challenge?.name}</span>
|
||||
) : (
|
||||
<select value={challenge?.name} onChange={(e) => handleChallengeTypeChange(index, e.target.value)} disabled={isReadOnly}>
|
||||
{challengesNames.map((challenge) => (
|
||||
{challengesNames?.map((challenge) => (
|
||||
<option key={challenge} value={challenge}>
|
||||
{challenge}
|
||||
</option>
|
||||
Reference in New Issue
Block a user