basic router layout, error boundary

This commit is contained in:
f0x 2022-09-09 01:39:43 +02:00
parent 0af6789ef3
commit d0ace4c26a
9 changed files with 325 additions and 28 deletions

View file

@ -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"

View 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>
);
};

View 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>
);
};

View file

@ -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>

View file

@ -129,6 +129,13 @@ input, select, textarea {
.error {
font-weight: bold;
pre {
background: $bg;
padding: 1rem;
overflow: auto;
margin: 0;
}
}
.hidden {

View 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>
);
};

View file

@ -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}&apos;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>
);
};

View file

@ -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"