Add global header

Improve profile layout
This commit is contained in:
Kieran 2022-02-27 21:25:10 +00:00
parent 40e70f8ffa
commit cb4ec890d4
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
14 changed files with 154 additions and 70 deletions

View File

@ -1,17 +1,17 @@
import {useDispatch, useSelector} from "react-redux";
import {Login} from "../Login";
import {FileList} from "./FileList";
import {UserList} from "./UserList";
import "./Admin.css";
import {logout} from "../LoginState";
import {Navigate} from "react-router-dom";
export function Admin() {
const auth = useSelector((state) => state.login.jwt);
const dispatch = useDispatch();
if (!auth) {
return <Login/>;
return <Navigate to="/login"/>;
} else {
return (
<div className="admin">

View File

@ -4,16 +4,18 @@ import store from "./Store";
import {FilePreview} from "./FilePreview";
import {HomePage} from "./HomePage";
import {Admin} from "./Admin/Admin";
import './App.css';
import {UserLogin} from "./UserLogin";
import {Profile} from "./Profile";
import {Header} from "./Header";
import './App.css';
function App() {
return (
<div className="app">
<Provider store={store}>
<BrowserRouter>
<Header/>
<Routes>
<Route exact path="/" element={<HomePage/>}/>
<Route exact path="/login" element={<UserLogin/>}/>

View File

@ -4,7 +4,7 @@
justify-content: center;
border-radius: 20px;
border: 2px dashed;
margin: 10vh 2px 2px;
margin: 5vh 2px 2px;
text-align: center;
user-select: none;
cursor: pointer;

View File

@ -9,7 +9,7 @@ import {useApi} from "./Api";
import {Helmet} from "react-helmet";
import {FormatBytes} from "./Util";
import {ApiHost} from "./Const";
import {FileUploader} from "./FileUploader";
import {InlineProfile} from "./InlineProfile";
export function FilePreview() {
const {Api} = useApi();
@ -122,7 +122,7 @@ export function FilePreview() {
{FormatBytes(info?.metadata?.size ?? 0, 2)}
</div>
</div>
{info.uploader ? <FileUploader uploader={info.uploader}/> : null}
{info.uploader ? <InlineProfile profile={info.uploader}/> : null}
<FileEdit file={info}/>
</Fragment>
) : "Not Found"}

View File

@ -1,26 +0,0 @@
import {DefaultAvatar} from "./Const";
import {Link} from "react-router-dom";
import "./FileUploader.css";
export function FileUploader(props) {
const uploader = props.uploader;
let avatarUrl = uploader.avatar ?? DefaultAvatar;
if(!avatarUrl.startsWith("http")){
avatarUrl = `/d/${avatarUrl}`;
}
let avatarStyles = {
backgroundImage: `url(${avatarUrl})`
};
return (
<div className="uploader-info">
<Link to={`/u/${uploader.id}`}>
<div className="small-profile">
<div className="avatar" style={avatarStyles}/>
<div className="name">{uploader.displayName}</div>
</div>
</Link>
</div>
)
}

View File

@ -1,21 +1,12 @@
import "./FooterLinks.css"
import StrikeLogo from "./image/strike.png";
import {Link} from "react-router-dom";
import {useSelector} from "react-redux";
export function FooterLinks(){
const auth = useSelector(state => state.login.jwt);
const profile = useSelector(state => state.login.profile);
export function FooterLinks(){
return (
<div className="footer">
<a href="https://discord.gg/8BkxTGs" target="_blank">Discord</a>
<a href="https://invite.strike.me/KS0FYF" target="_blank">Get Strike <img src={StrikeLogo} alt="Strike logo"/> </a>
<a href="https://github.com/v0l/void.cat" target="_blank">GitHub</a>
{!auth ?
<Link to={"/login"}>Login</Link> :
<Link to={`/u/${profile?.id}`}>Profile</Link>
}
</div>
);
}

View File

@ -0,0 +1,12 @@
.header {
user-select: none;
display: flex;
padding: 5px 0;
align-items: center;
}
.header .title {
font-size: 30px;
line-height: 2;
flex-grow: 1;
}

23
VoidCat/spa/src/Header.js Normal file
View File

@ -0,0 +1,23 @@
import "./Header.css";
import {Link} from "react-router-dom";
import {useSelector} from "react-redux";
import {InlineProfile} from "./InlineProfile";
export function Header() {
const profile = useSelector(state => state.login.profile);
return (
<div className="header page">
<div className="title">
<Link to="/">void.cat</Link>
</div>
{profile ?
<InlineProfile profile={profile} options={{
showName: false
}}/> :
<Link to="/login">
<div className="btn">Login</div>
</Link>}
</div>
)
}

View File

@ -1,14 +1,10 @@
.uploader-info {
margin-top: 10px;
text-align: start;
}
.uploader-info .small-profile {
.small-profile {
display: inline-flex;
align-items: center;
}
.uploader-info .small-profile .avatar {
.small-profile .avatar {
width: 64px;
height: 64px;
border-radius: 16px;
@ -17,6 +13,6 @@
background-repeat: no-repeat;
}
.uploader-info .small-profile .name {
.small-profile .name {
padding-left: 15px;
}

View File

@ -0,0 +1,38 @@
import "./InlineProfile.css";
import {DefaultAvatar} from "./Const";
import {Link} from "react-router-dom";
const DefaultSize = 64;
export function InlineProfile(props) {
const profile = props.profile;
const options = {
size: DefaultSize,
showName: true,
link: true,
...props.options
};
let avatarUrl = profile.avatar ?? DefaultAvatar;
if (!avatarUrl.startsWith("http")) {
avatarUrl = `/d/${avatarUrl}`;
}
let avatarStyles = {
backgroundImage: `url(${avatarUrl})`
};
if (options.size !== DefaultSize) {
avatarStyles.width = `${options.size}px`;
avatarStyles.height = `${options.size}px`;
}
let elms = (
<div className="small-profile">
<div className="avatar" style={avatarStyles}/>
{options.showName ? <div className="name">{profile.displayName}</div> : null}
</div>
);
if (options.link === true) {
return <Link to={`/u/${profile.id}`}>{elms}</Link>
}
return elms;
}

View File

@ -22,6 +22,7 @@ export const LoginState = createSlice({
},
logout: (state) => {
state.jwt = null;
state.profile = null;
window.localStorage.removeItem(LocalStorageKey);
window.localStorage.removeItem(LocalStorageProfileKey);
}

View File

@ -39,6 +39,10 @@
opacity: 1;
}
.profile .roles {
margin: 20px 0;
.profile .roles > span {
margin-right: 10px;
}
.profile dt {
font-weight: bold;
}

View File

@ -4,12 +4,15 @@ import {useApi} from "./Api";
import {ApiHost, DefaultAvatar} from "./Const";
import "./Profile.css";
import {useDispatch, useSelector} from "react-redux";
import {setProfile as setGlobalProfile} from "./LoginState";
import {logout, setProfile as setGlobalProfile} from "./LoginState";
import {DigestAlgo} from "./FileUpload";
import {buf2hex} from "./Util";
import moment from "moment";
import FeatherIcon from "feather-icons-react";
export function Profile() {
const [profile, setProfile] = useState();
const [saved, setSaved] = useState(false);
const auth = useSelector(state => state.login.jwt);
const localProfile = useSelector(state => state.login.profile);
const canEdit = localProfile?.id === profile?.id;
@ -52,7 +55,7 @@ export function Profile() {
const file = res[0];
const buf = await file.arrayBuffer();
const digest = await crypto.subtle.digest(DigestAlgo, buf);
let req = await fetch(`${ApiHost}/upload`, {
mode: "cors",
method: "POST",
@ -65,19 +68,19 @@ export function Profile() {
"Authorization": `Bearer ${auth}`
}
});
if(req.ok) {
if (req.ok) {
let rsp = await req.json();
if(rsp.ok) {
if (rsp.ok) {
setProfile({
...profile,
avatar: rsp.file.id
});
}
}
}
}
async function saveUser() {
let r = await Api.updateUser({
id: profile.id,
@ -88,6 +91,7 @@ export function Profile() {
if (r.ok) {
// saved
dispatch(setGlobalProfile(profile));
setSaved(true);
}
}
@ -95,9 +99,15 @@ export function Profile() {
loadProfile();
}, []);
useEffect(() => {
if (saved === true) {
setTimeout(() => setSaved(false), 1000);
}
}, [saved]);
if (profile) {
let avatarUrl = profile.avatar ?? DefaultAvatar;
if(!avatarUrl.startsWith("http")) {
if (!avatarUrl.startsWith("http")) {
// assume void-cat hosted avatar
avatarUrl = `/d/${avatarUrl}`;
}
@ -113,14 +123,24 @@ export function Profile() {
onChange={(e) => editUsername(e.target.value)}/>
: profile.displayName}
</div>
<div className="avatar" style={avatarStyles}>
{canEdit ? <div className="edit-avatar" onClick={() => changeAvatar()}>
<h3>Edit</h3>
</div> : null}
</div>
<div className="roles">
<h3>Roles:</h3>
{profile.roles.map(a => <span className="btn">{a}</span>)}
<div className="flex">
<div className="flx-1">
<div className="avatar" style={avatarStyles}>
{canEdit ? <div className="edit-avatar" onClick={() => changeAvatar()}>
<h3>Edit</h3>
</div> : null}
</div>
</div>
<div className="flx-1">
<dl>
<dt>Created</dt>
<dd>{moment(profile.created).fromNow()}</dd>
<dt>Roles</dt>
<dd>{profile.roles.map(a => <span key={a} className="btn">{a}</span>)}</dd>
<dt>Files</dt>
<dd>0</dd>
</dl>
</div>
</div>
{canEdit ?
<Fragment>
@ -129,7 +149,17 @@ export function Profile() {
<input type="checkbox" checked={profile.public}
onChange={(e) => editPublic(e.target.checked)}/>
</p>
<div className="btn" onClick={saveUser}>Save</div>
<div className="flex flex-center">
<div>
<div className="btn" onClick={saveUser}>Save</div>
</div>
<div>
{saved ? <FeatherIcon icon="check-circle"/> : null}
</div>
<div>
<div className="btn" onClick={() => dispatch(logout())}>Logout</div>
</div>
</div>
</Fragment> : null}
</div>
</div>

View File

@ -29,4 +29,17 @@ a:hover {
padding: 10px 20px;
user-select: none;
cursor: pointer;
margin: 5px;
}
.flex {
display: flex;
}
.flx-1 {
flex: 1;
}
.flex-center {
align-items: center;
}