mirror of
https://github.com/plebbit/seedit.git
synced 2026-06-12 10:06:05 -04:00
feat(subplebbit settings): add json editor
This commit is contained in:
@@ -224,7 +224,7 @@
|
||||
"add_moderator": "add a moderator",
|
||||
"add_rule": "add a rule",
|
||||
"json_settings": "JSON settings",
|
||||
"json_settings_info": "quickly copy or paste the community settings",
|
||||
"json_settings_info": "quickly copy and paste the community settings",
|
||||
"address_setting_info": "Set a readable community address using a crypto domain",
|
||||
"enter_crypto_address": "Please enter a valid crypto address.",
|
||||
"check_for_updates": "<1>Check</1> for updates",
|
||||
|
||||
@@ -15,6 +15,7 @@ import PostPage from './views/post-page';
|
||||
import Profile from './views/profile';
|
||||
import Settings from './views/settings';
|
||||
import AccountDataEditor from './views/settings/account-data-editor';
|
||||
import SubplebbitDataEditor from './views/subplebbit-settings/subplebbit-data-editor';
|
||||
import SubmitPage from './views/submit-page';
|
||||
import Subplebbit from './views/subplebbit';
|
||||
import SubplebbitSettings from './views/subplebbit-settings';
|
||||
@@ -97,6 +98,7 @@ const App = () => {
|
||||
|
||||
<Route path='/settings' element={<Settings />} />
|
||||
<Route path='/p/:subplebbitAddress/settings' element={<SubplebbitSettings />} />
|
||||
<Route path='/p/:subplebbitAddress/settings/editor' element={<SubplebbitDataEditor />} />
|
||||
<Route path='/settings/plebbit-options' element={<Settings />} />
|
||||
<Route path='/settings/content-options' element={<Settings />} />
|
||||
<Route path='/settings/account-data' element={<AccountDataEditor />} />
|
||||
|
||||
@@ -176,7 +176,7 @@ export const isSubplebbitAboutView = (pathname: string, params: ParamsType): boo
|
||||
};
|
||||
|
||||
export const isSubplebbitSettingsView = (pathname: string, params: ParamsType): boolean => {
|
||||
return params.subplebbitAddress ? pathname === `/p/${params.subplebbitAddress}/settings` : false;
|
||||
return params.subplebbitAddress ? pathname === `/p/${params.subplebbitAddress}/settings` || pathname === `/p/${params.subplebbitAddress}/settings/editor` : false;
|
||||
};
|
||||
|
||||
export const isSubplebbitSubmitView = (pathname: string, params: ParamsType): boolean => {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './subplebbit-data-editor';
|
||||
@@ -0,0 +1,211 @@
|
||||
import React, { useEffect, useMemo, useState, lazy, Suspense, Component } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { usePublishSubplebbitEdit, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import useTheme from '../../../stores/use-theme-store';
|
||||
import styles from '../../settings/account-data-editor/account-data-editor.module.css';
|
||||
import useIsMobile from '../../../hooks/use-is-mobile';
|
||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||
import useSubplebbitSettingsStore from '../../../stores/use-subplebbit-settings-store';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import ErrorDisplay from '../../../components/error-display';
|
||||
import useStateString from '../../../hooks/use-state-string';
|
||||
|
||||
class EditorErrorBoundary extends Component<{ children: React.ReactNode; fallback: React.ReactNode }> {
|
||||
constructor(props: { children: React.ReactNode; fallback: React.ReactNode }) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error: any, errorInfo: any) {
|
||||
console.error('Ace Editor failed to load:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if ((this.state as any).hasError) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const LazyAceEditor = lazy(async () => {
|
||||
const ReactAceModule = await import('react-ace');
|
||||
await import('ace-builds/src-noconflict/mode-json');
|
||||
await import('ace-builds/src-noconflict/theme-github');
|
||||
await import('ace-builds/src-noconflict/theme-tomorrow_night');
|
||||
return ReactAceModule;
|
||||
});
|
||||
|
||||
const FallbackEditor = ({ value, onChange, height, disabled }: { value: string; onChange: (value: string) => void; height: string; disabled?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.infobar}>{t('editor_fallback_warning', 'Advanced editor failed to load. Using basic text editor as fallback.')}</div>
|
||||
<textarea value={value} onChange={(e) => onChange(e.target.value)} className={styles.fallbackEditor} style={{ height }} spellCheck={false} disabled={disabled} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SubplebbitDataEditor = () => {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsMobile();
|
||||
const theme = useTheme((state) => state.theme);
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
|
||||
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||
const { address, challenges, createdAt, description, error, rules, settings, suggested, roles, title } = subplebbit || {};
|
||||
const hasLoaded = !!createdAt;
|
||||
|
||||
const { publishSubplebbitEditOptions, setSubplebbitSettingsStore, resetSubplebbitSettingsStore } = useSubplebbitSettingsStore();
|
||||
|
||||
const { error: publishSubplebbitEditError, publishSubplebbitEdit } = usePublishSubplebbitEdit(publishSubplebbitEditOptions);
|
||||
|
||||
const subplebbitSettings = useMemo(
|
||||
() => JSON.stringify({ title, description, address, suggested, rules, roles, settings, challenges, subplebbitAddress }, null, 2),
|
||||
[title, description, address, suggested, rules, roles, settings, challenges, subplebbitAddress],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setText(subplebbitSettings);
|
||||
}, [subplebbitSettings]);
|
||||
|
||||
const [showSaving, setShowSaving] = useState(false);
|
||||
const [currentError, setCurrentError] = useState<Error | undefined>(undefined);
|
||||
|
||||
const saveSubplebbitSettings = async () => {
|
||||
try {
|
||||
setShowSaving(true);
|
||||
setCurrentError(undefined);
|
||||
console.log('Saving subplebbit with options:', publishSubplebbitEditOptions);
|
||||
await publishSubplebbitEdit();
|
||||
setShowSaving(false);
|
||||
if (publishSubplebbitEditError) {
|
||||
setCurrentError(publishSubplebbitEditError);
|
||||
alert(publishSubplebbitEditError.message || 'Error: ' + publishSubplebbitEditError);
|
||||
} else {
|
||||
alert(t('settings_saved', { subplebbitAddress }));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.warn(e);
|
||||
setCurrentError(e);
|
||||
alert(`failed editing subplebbit: ${e.message}`);
|
||||
} else {
|
||||
console.error('An unknown error occurred:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set store for loaded subplebbit settings when editing
|
||||
useEffect(() => {
|
||||
if (hasLoaded) {
|
||||
resetSubplebbitSettingsStore();
|
||||
setSubplebbitSettingsStore({
|
||||
title: title ?? '',
|
||||
description: description ?? '',
|
||||
address,
|
||||
suggested: suggested ?? {},
|
||||
rules: rules ?? [],
|
||||
roles: roles ?? {},
|
||||
settings: settings ?? {},
|
||||
challenges: challenges ?? [],
|
||||
subplebbitAddress,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
hasLoaded,
|
||||
resetSubplebbitSettingsStore,
|
||||
setSubplebbitSettingsStore,
|
||||
title,
|
||||
description,
|
||||
address,
|
||||
suggested,
|
||||
rules,
|
||||
roles,
|
||||
settings,
|
||||
challenges,
|
||||
subplebbitAddress,
|
||||
]);
|
||||
|
||||
const loadingStateString = useStateString(subplebbit);
|
||||
|
||||
if (!hasLoaded) {
|
||||
return (
|
||||
<>
|
||||
{error?.message && (
|
||||
<div className={styles.error}>
|
||||
<ErrorDisplay error={error} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.loading}>
|
||||
<LoadingEllipsis string={loadingStateString || t('loading')} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<EditorErrorBoundary
|
||||
fallback={<FallbackEditor value={text} onChange={setText} height={isMobile ? 'calc(80vh - 95px)' : 'calc(90vh - 77px)'} disabled={showSaving} />}
|
||||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className={styles.loading}>
|
||||
<LoadingEllipsis string={t('loading_editor')} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LazyAceEditor
|
||||
mode='json'
|
||||
theme={theme === 'dark' ? 'tomorrow_night' : 'github'}
|
||||
value={text}
|
||||
onChange={setText}
|
||||
name='ACCOUNT_DATA_EDITOR'
|
||||
editorProps={{ $blockScrolling: true }}
|
||||
className={styles.editor}
|
||||
width='100%'
|
||||
height={isMobile ? 'calc(80vh - 95px)' : 'calc(90vh - 77px)'}
|
||||
setOptions={{
|
||||
useWorker: false,
|
||||
enableBasicAutocompletion: false,
|
||||
enableLiveAutocompletion: false,
|
||||
enableSnippets: false,
|
||||
showPrintMargin: false,
|
||||
highlightActiveLine: true,
|
||||
showGutter: true,
|
||||
foldStyle: 'markbeginend',
|
||||
showFoldWidgets: true,
|
||||
readOnly: showSaving,
|
||||
}}
|
||||
fontSize={14}
|
||||
/>
|
||||
</Suspense>
|
||||
</EditorErrorBoundary>
|
||||
{currentError && <div className={styles.error}>error: {currentError.message || 'unknown error'}</div>}
|
||||
{showSaving ? (
|
||||
<div className={styles.loading}>
|
||||
<LoadingEllipsis string={t('saving')} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.buttons}>
|
||||
<Trans
|
||||
i18nKey='save_reset_changes'
|
||||
components={{
|
||||
1: <button key='saveSubplebbitSettingsButton' onClick={saveSubplebbitSettings} />,
|
||||
2: <button key='resetSubplebbitSettingsButton' onClick={() => setText(subplebbitSettings)} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubplebbitDataEditor;
|
||||
@@ -323,30 +323,15 @@ const Moderators = ({ isReadOnly = false }: { isReadOnly?: boolean }) => {
|
||||
|
||||
const JSONSettings = ({ isReadOnly = false }: { isReadOnly?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const { challenges, title, description, address, suggested, rules, roles, settings, subplebbitAddress, setSubplebbitSettingsStore } = useSubplebbitSettingsStore();
|
||||
const [text, setText] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const JSONSettings = JSON.stringify({ title, description, address, suggested, rules, roles, settings, challenges, subplebbitAddress }, null, 2);
|
||||
setText(JSONSettings);
|
||||
}, [challenges, title, description, address, suggested, rules, roles, settings, subplebbitAddress]);
|
||||
|
||||
const handleChange = (newText: string) => {
|
||||
setText(newText);
|
||||
try {
|
||||
const newSettings = JSON.parse(newText);
|
||||
setSubplebbitSettingsStore(newSettings);
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON format');
|
||||
}
|
||||
};
|
||||
const navigate = useNavigate();
|
||||
const { subplebbitAddress } = useParams<{ subplebbitAddress: string }>();
|
||||
|
||||
return (
|
||||
<div className={`${styles.box}`}>
|
||||
<div className={`${styles.boxTitle} ${styles.JSONSettingsTitle}`}>{t('json_settings')}</div>
|
||||
<div className={styles.boxSubtitle}>{t('json_settings_info')}</div>
|
||||
<div className={`${styles.boxInput} ${styles.JSONSettings}`}>
|
||||
<textarea onChange={(e) => handleChange(e.target.value)} autoCorrect='off' autoComplete='off' spellCheck='false' value={text} disabled={isReadOnly} />
|
||||
<button onClick={() => navigate(`/p/${subplebbitAddress}/settings/editor`)}>{t('edit')}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user