mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
basic router layout, error boundary
This commit is contained in:
parent
0af6789ef3
commit
d0ace4c26a
9 changed files with 325 additions and 28 deletions
|
@ -34,6 +34,7 @@
|
|||
"pretty-bytes": "^5.6.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"reactify": "^1.1.1",
|
||||
"uglifyify": "^5.0.2",
|
||||
"wouter": "^2.8.0-alpha.2"
|
||||
|
|
33
web/source/settings-panel/admin/index.js
Normal file
33
web/source/settings-panel/admin/index.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const { Route, Switch } = require("wouter");
|
||||
|
||||
module.exports = function AdminPanel({oauth, routes}) {
|
||||
return (
|
||||
<Switch>
|
||||
{routes.map(([path, component]) => {
|
||||
return <Route key={path} path={path} component={component}/>;
|
||||
})}
|
||||
</Switch>
|
||||
);
|
||||
};
|
45
web/source/settings-panel/components/error.js
Normal file
45
web/source/settings-panel/components/error.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
|
||||
module.exports = function ErrorFallback({error, resetErrorBoundary}) {
|
||||
return (
|
||||
<div className="error">
|
||||
<p>
|
||||
{"An error occured, please report this on the "}
|
||||
<a href="https://github.com/superseriousbusiness/gotosocial/issues">GoToSocial issue tracker</a>
|
||||
{" or "}
|
||||
<a href="https://matrix.to/#/#gotosocial-help:superseriousbusiness.org">Matrix support room</a>.
|
||||
<br/>Include the details below:
|
||||
</p>
|
||||
<pre>
|
||||
{error.name}: {error.message}
|
||||
</pre>
|
||||
<pre>
|
||||
{error.stack}
|
||||
</pre>
|
||||
<p>
|
||||
<button onClick={resetErrorBoundary}>Try again</button> or <a href="">refresh the page</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -22,23 +22,35 @@ const Promise = require("bluebird");
|
|||
const React = require("react");
|
||||
const ReactDom = require("react-dom");
|
||||
const { Link, Route, Switch, useRoute, Redirect } = require("wouter");
|
||||
const { ErrorBoundary } = require("react-error-boundary");
|
||||
|
||||
const Auth = require("./components/auth");
|
||||
const ErrorFallback = require("./components/error");
|
||||
|
||||
const oauthLib = require("./lib/oauth");
|
||||
|
||||
require("./style.css");
|
||||
|
||||
const UserPanel = require("./user");
|
||||
const AdminPanel = require("./admin");
|
||||
|
||||
const nav = {
|
||||
"User": [
|
||||
["Profile", require("./user/profile.js")],
|
||||
["Settings", require("./user/settings.js")],
|
||||
["Customization", require("./user/customization.js")]
|
||||
],
|
||||
"Admin": [
|
||||
["Instance Settings", require("./admin/settings.js")],
|
||||
["Federation", require("./admin/federation.js")],
|
||||
["Customization", require("./admin/customization.js")]
|
||||
]
|
||||
"User": {
|
||||
Component: require("./user"),
|
||||
entries: {
|
||||
"Profile": require("./user/profile.js"),
|
||||
"Settings": require("./user/settings.js"),
|
||||
"Customization": require("./user/customization.js")
|
||||
}
|
||||
},
|
||||
"Admin": {
|
||||
Component: require("./admin"),
|
||||
entries: {
|
||||
"Instance Settings": require("./admin/settings.js"),
|
||||
"Federation": require("./admin/federation.js"),
|
||||
"Customization": require("./admin/customization.js")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function urlSafe(str) {
|
||||
|
@ -49,31 +61,50 @@ function urlSafe(str) {
|
|||
const sidebar = [];
|
||||
const panelRouter = [];
|
||||
|
||||
Object.entries(nav).forEach(([category, entries]) => {
|
||||
let base = `/settings/${urlSafe(category)}`;
|
||||
// Generate component tree from `nav` object once, as it won't change
|
||||
Object.entries(nav).forEach(([name, {Component, entries}]) => {
|
||||
let base = `/settings/${urlSafe(name)}`;
|
||||
|
||||
let links = [];
|
||||
let routes = [];
|
||||
|
||||
let firstRoute;
|
||||
|
||||
Object.entries(entries).forEach(([name, component]) => {
|
||||
let url = `${base}/${urlSafe(name)}`;
|
||||
|
||||
if (firstRoute == undefined) {
|
||||
firstRoute = `${base}/${urlSafe(name)}`;
|
||||
}
|
||||
|
||||
routes.push([url, component]);
|
||||
|
||||
links.push(
|
||||
<NavButton key={url} href={url} name={name} />
|
||||
);
|
||||
});
|
||||
|
||||
// Category header goes to first page in category
|
||||
panelRouter.push(
|
||||
<Route key={base} path={base}>
|
||||
<Redirect to={`${base}/${urlSafe(entries[0][0])}`}/>
|
||||
<Redirect to={firstRoute}/>
|
||||
</Route>
|
||||
);
|
||||
|
||||
let links = entries.map(([name, component]) => {
|
||||
let url = `${base}/${urlSafe(name)}`;
|
||||
|
||||
panelRouter.push(
|
||||
<Route key={url} path={url} component={component}/>
|
||||
);
|
||||
|
||||
return <NavButton key={url} href={url} name={name} />;
|
||||
});
|
||||
let childrenPath = `${base}/:section`;
|
||||
panelRouter.push(
|
||||
<Route key={childrenPath} path={childrenPath}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => {}}>
|
||||
{/* FIXME: implement onReset */}
|
||||
<Component routes={routes}/>
|
||||
</ErrorBoundary>
|
||||
</Route>
|
||||
);
|
||||
|
||||
sidebar.push(
|
||||
<React.Fragment key={category}>
|
||||
<Link href={`${base}/${urlSafe(entries[0][0])}`}>
|
||||
<React.Fragment key={name}>
|
||||
<Link href={firstRoute}>
|
||||
<a>
|
||||
<h2>{category}</h2>
|
||||
<h2>{name}</h2>
|
||||
</a>
|
||||
</Link>
|
||||
<nav>
|
||||
|
|
|
@ -129,6 +129,13 @@ input, select, textarea {
|
|||
|
||||
.error {
|
||||
font-weight: bold;
|
||||
|
||||
pre {
|
||||
background: $bg;
|
||||
padding: 1rem;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
|
|
51
web/source/settings-panel/user/index.js
Normal file
51
web/source/settings-panel/user/index.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const { Route, Switch } = require("wouter");
|
||||
|
||||
module.exports = function UserPanel({oauth, routes}) {
|
||||
// const [account, setAccount] = React.useState({});
|
||||
// const [errorMsg, setError] = React.useState("");
|
||||
// const [statusMsg, setStatus] = React.useState("Fetching user info");
|
||||
|
||||
// React.useEffect(() => {
|
||||
// Promise.try(() => {
|
||||
// return oauth.apiRequest("/api/v1/accounts/verify_credentials", "GET");
|
||||
// }).then((json) => {
|
||||
// setAccount(json);
|
||||
// }).catch((e) => {
|
||||
// setError(e.message);
|
||||
// setStatus("");
|
||||
// });
|
||||
// }, [oauth, setAccount, setError, setStatus]);
|
||||
|
||||
// throw new Error("test");
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
{routes.map(([path, component]) => {
|
||||
console.log(component);
|
||||
return <Route key={path} path={path} component={component}/>;
|
||||
})}
|
||||
</Switch>
|
||||
);
|
||||
};
|
|
@ -18,6 +18,121 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
module.exports = function UserProfile() {
|
||||
return "user profile";
|
||||
const Promise = require("bluebird");
|
||||
const React = require("react");
|
||||
const { useErrorHandler } = require("react-error-boundary");
|
||||
|
||||
const Submit = require("../components/submit");
|
||||
|
||||
module.exports = function UserProfile({account, oauth}) {
|
||||
const [errorMsg, setError] = React.useState("");
|
||||
const [statusMsg, setStatus] = React.useState("");
|
||||
|
||||
const [headerFile, setHeaderFile] = React.useState(undefined);
|
||||
const [headerSrc, setHeaderSrc] = React.useState("");
|
||||
|
||||
const [avatarFile, setAvatarFile] = React.useState(undefined);
|
||||
const [avatarSrc, setAvatarSrc] = React.useState("");
|
||||
|
||||
const [displayName, setDisplayName] = React.useState("");
|
||||
const [bio, setBio] = React.useState("");
|
||||
const [locked, setLocked] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setHeaderSrc(account.header);
|
||||
setAvatarSrc(account.avatar);
|
||||
|
||||
setDisplayName(account.display_name);
|
||||
setBio(account.source ? account.source.note : "");
|
||||
setLocked(account.locked);
|
||||
}, [account, setHeaderSrc, setAvatarSrc, setDisplayName, setBio, setLocked]);
|
||||
|
||||
const headerOnChange = (e) => {
|
||||
setHeaderFile(e.target.files[0]);
|
||||
setHeaderSrc(URL.createObjectURL(e.target.files[0]));
|
||||
};
|
||||
|
||||
const avatarOnChange = (e) => {
|
||||
setAvatarFile(e.target.files[0]);
|
||||
setAvatarSrc(URL.createObjectURL(e.target.files[0]));
|
||||
};
|
||||
|
||||
const submit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
setStatus("PATCHing");
|
||||
setError("");
|
||||
return Promise.try(() => {
|
||||
let formDataInfo = new FormData();
|
||||
|
||||
if (headerFile) {
|
||||
formDataInfo.set("header", headerFile);
|
||||
}
|
||||
|
||||
if (avatarFile) {
|
||||
formDataInfo.set("avatar", avatarFile);
|
||||
}
|
||||
|
||||
formDataInfo.set("display_name", displayName);
|
||||
formDataInfo.set("note", bio);
|
||||
formDataInfo.set("locked", locked);
|
||||
|
||||
return oauth.apiRequest("/api/v1/accounts/update_credentials", "PATCH", formDataInfo, "form");
|
||||
}).then((json) => {
|
||||
setStatus("Saved!");
|
||||
|
||||
setHeaderSrc(json.header);
|
||||
setAvatarSrc(json.avatar);
|
||||
|
||||
setDisplayName(json.display_name);
|
||||
setBio(json.source.note);
|
||||
setLocked(json.locked);
|
||||
}).catch((e) => {
|
||||
setError(e.message);
|
||||
setStatus("");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="basic">
|
||||
<h1>@{account.username}'s Profile Info</h1>
|
||||
<form>
|
||||
<div className="labelinput">
|
||||
<label htmlFor="header">Header</label>
|
||||
<div className="border">
|
||||
<img className="headerpreview" src={headerSrc} alt={headerSrc ? `header image for ${account.username}` : "None set"}/>
|
||||
<div>
|
||||
<label htmlFor="header" className="file-input button">Browse…</label>
|
||||
<span>{headerFile ? headerFile.name : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
<input className="hidden" id="header" type="file" accept="image/*" onChange={headerOnChange}/>
|
||||
</div>
|
||||
<div className="labelinput">
|
||||
<label htmlFor="avatar">Avatar</label>
|
||||
<div className="border">
|
||||
<img className="avatarpreview" src={avatarSrc} alt={headerSrc ? `avatar image for ${account.username}` : "None set"}/>
|
||||
<div>
|
||||
<label htmlFor="avatar" className="file-input button">Browse…</label>
|
||||
<span>{avatarFile ? avatarFile.name : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
<input className="hidden" id="avatar" type="file" accept="image/*" onChange={avatarOnChange}/>
|
||||
</div>
|
||||
<div className="labelinput">
|
||||
<label htmlFor="displayname">Display Name</label>
|
||||
<input id="displayname" type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} placeholder="A GoToSocial user"/>
|
||||
</div>
|
||||
<div className="labelinput">
|
||||
<label htmlFor="bio">Bio</label>
|
||||
<textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Just trying out GoToSocial, my pronouns are they/them and I like sloths."/>
|
||||
</div>
|
||||
<div className="labelcheckbox">
|
||||
<label htmlFor="locked">Manually approve follow requests</label>
|
||||
<input id="locked" type="checkbox" checked={locked} onChange={(e) => setLocked(e.target.checked)}/>
|
||||
</div>
|
||||
<Submit onClick={submit} label="Save profile info" errorMsg={errorMsg} statusMsg={statusMsg}/>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
};
|
|
@ -920,6 +920,13 @@
|
|||
"@babel/plugin-transform-react-jsx-development" "^7.18.6"
|
||||
"@babel/plugin-transform-react-pure-annotations" "^7.18.6"
|
||||
|
||||
"@babel/runtime@^7.12.5":
|
||||
version "7.19.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
|
||||
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@babel/runtime@^7.8.4":
|
||||
version "7.18.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
|
||||
|
@ -5078,6 +5085,13 @@ react-dom@^17.0.1:
|
|||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-error-boundary@^3.1.4:
|
||||
version "3.1.4"
|
||||
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
|
||||
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
|
Loading…
Reference in a new issue