I did a thing

- Fixed random things.
- Added a footer.
- Changed menu styling to be more pleasing.
- Made menu turn into hamburger dropdown menu so this dashboard is somewhat okay to use on mobile.
- Made a log out menu button for the mobile people.
- Made the entire layout work on mobile.
- Fixed the shit I broke along the way.
It's 2:30am now, time for bed.
This commit is contained in:
D3visionNL 2022-11-27 02:23:30 +01:00
parent d9a9ecafbb
commit 2dfb996cde
11 changed files with 185 additions and 31 deletions

View File

@ -8,6 +8,7 @@
"react-router": "^6.4.3", "react-router": "^6.4.3",
"react-router-dom": "^6.4.3", "react-router-dom": "^6.4.3",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-top-loading-bar": "^2.3.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"devDependencies": { "devDependencies": {

View File

@ -55,7 +55,7 @@ function App() {
</div> </div>
: null} : null}
<div className='main-content'> <div className={`main-content ${user ? "" : "login"}`}>
<ErrorBoundary> <ErrorBoundary>
<Routes> <Routes>
@ -87,7 +87,10 @@ function App() {
</Routes> </Routes>
</ErrorBoundary> </ErrorBoundary>
<div className={`footer ${user ? "" : "login"}`}>
<p className='m-0'>Made with by <a href="https://corgi.wtf" target="_blank" rel="noreferrer">Navy.gif</a> &nbsp;|&nbsp; CSS by <a href="https://d3vision.dev" target="_blank" rel="noreferrer">D3vision</a></p>
</div>
</div> </div>
</BrowserRouter> </BrowserRouter>

View File

@ -2,6 +2,9 @@ import React from 'react';
import '../css/components/Sidebar.css'; import '../css/components/Sidebar.css';
import NavLink from '../structures/NavLink'; import NavLink from '../structures/NavLink';
import logoImg from '../img/logo.png'; import logoImg from '../img/logo.png';
import { useNavigate } from "react-router";
import { useLoginContext } from "../structures/UserContext";
import { clearSession, post } from "../util/Util";
const Sidebar = ({children}) => { const Sidebar = ({children}) => {
@ -15,9 +18,32 @@ const expandMenu = (event) => {
event.preventDefault(); event.preventDefault();
event.target.parentElement.classList.toggle("open"); event.target.parentElement.classList.toggle("open");
}; };
const expandMobileMenu = (event) => {
event.preventDefault();
event.target.parentElement.parentElement.classList.toggle("show-menu");
};
const closeMobileMenu = (event) => {
if(event.target.classList.contains("sidebar-menu-item-carrot")) return;
event.target.getRootNode().querySelector(".sidebar").classList.remove("show-menu");
};
// Nav menu // Nav menu
const SidebarMenu = ({menuItems = [], children}) => { const SidebarMenu = ({menuItems = [], children}) => {
const [user, updateUser] = useLoginContext();
const navigate = useNavigate();
if (!user) return;
const logOut = async () => {
const response = await post('/api/logout');
if (response.status === 200) {
clearSession();
updateUser();
navigate('/login');
}
};
const elements = []; const elements = [];
for (const menuItem of menuItems) { for (const menuItem of menuItems) {
@ -26,10 +52,10 @@ const SidebarMenu = ({menuItems = [], children}) => {
let subElements = null; let subElements = null;
if (subItems.length) subElements = subItems.map(({ label, to, relative = true }) => { if (subItems.length) subElements = subItems.map(({ label, to, relative = true }) => {
if(relative) to = `${menuItem.to}${to}`; if(relative) to = `${menuItem.to}${to}`;
return <NavLink className='sidebar-menu-item sidebar-menu-child-item' activeClassName='active' to={to} key={label}>{label}</NavLink>; return <NavLink className='sidebar-menu-item sidebar-menu-child-item' onClick={closeMobileMenu} activeClassName='active' to={to} key={label}>{label}</NavLink>;
}); });
elements.push(<div key={label} className='parent-menu'> elements.push(<div key={label} className='parent-menu'>
<NavLink className={`sidebar-menu-item ${subElements?.length ? 'has-children' : ''}`} activeClassName='active' {...rest} key={label}> <NavLink className={`sidebar-menu-item ${subElements?.length ? 'has-children' : ''}`} onClick={closeMobileMenu} activeClassName='active' {...rest} key={label}>
{label}{subElements && <i className="sidebar-menu-item-carrot" onClick={expandMenu}></i>} {label}{subElements && <i className="sidebar-menu-item-carrot" onClick={expandMenu}></i>}
</NavLink> </NavLink>
{subElements && <div className='sidebar-menu-child-wrapper'> {subElements && <div className='sidebar-menu-child-wrapper'>
@ -39,11 +65,14 @@ const SidebarMenu = ({menuItems = [], children}) => {
} }
return <div className='sidebar-menu'> return <div className='sidebar-menu'>
<span className='hamburger' onClick={expandMobileMenu}></span>
<img className="sidebar-logo" src={logoImg}/> <img className="sidebar-logo" src={logoImg}/>
<hr/>
{elements} {elements}
{children} {children}
<div className="parent-menu log-out-menu-item">
<a className="sidebar-menu-item" onClick={logOut} href="#">Log Out</a>
</div>
</div>; </div>;
}; };

View File

@ -22,12 +22,27 @@ header {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: var(--background-secondary); background-color: var(--background-secondary);
padding-bottom: 1rem;
} }
.page{ .page{
padding: 0 15px 0 245px; padding: 0 15px 0 245px;
} }
.footer{
padding: 15px 15px 15px 245px;
font-size: 14px;
display: flex;
justify-content: center;
}
@media screen and (max-width: 768px){
.footer{
padding: 15px;
font-size: inherit;
}
}
.ld{ .ld{
min-height: 140px; min-height: 140px;
position: relative; position: relative;

View File

@ -3,22 +3,29 @@
position: fixed; position: fixed;
width: 230px; width: 230px;
padding: 1rem 0; padding: 1rem 0;
z-index: 1000;
border-radius: 0;
} }
.sidebar-menu { .sidebar-menu {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.sidebar-menu .parent-menu:first-of-type{
border-top: 1px solid #232323;
}
.parent-menu{ .parent-menu{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-bottom: 1px solid #232323;
} }
.sidebar-logo{ .sidebar-logo{
margin: auto; margin: auto;
margin-top: 1rem; margin-top: 2.5rem;
margin-bottom: 1rem; margin-bottom: 3.5rem;
max-width: 170px; max-height: 80px;
} }
.sidebar-menu-item-carrot{ .sidebar-menu-item-carrot{
@ -33,15 +40,16 @@
} }
.sidebar-menu-item.has-children.open .sidebar-menu-item-carrot{ .sidebar-menu-item.has-children.open .sidebar-menu-item-carrot{
transform: rotate(-90deg); transform: rotate(-90deg);
transition: transform ease-in-out 0.15s; transition: transform ease-in-out 0.2s;
} }
.sidebar-menu-item.has-children.active .sidebar-menu-item-carrot{ .sidebar-menu-item.has-children.active .sidebar-menu-item-carrot{
margin-right: 5px; margin-right: 5px;
} }
.sidebar-menu-item.has-children.open + .sidebar-menu-child-wrapper{ .sidebar-menu-item.has-children.open + .sidebar-menu-child-wrapper{
height: 100%; height: 100%;
opacity: 1; /* opacity: 1; */
transition: max-height 0.5s ease-in-out, opacity 0.25s ease-in-out; /* transition: max-height 0.5s ease-in-out, opacity 0.25s ease-in-out; */
transition: max-height ease-in-out 0.25s;
max-height: 600px; max-height: 600px;
} }
.sidebar-menu-item.sidebar-menu-item.has-children{ .sidebar-menu-item.sidebar-menu-item.has-children{
@ -58,8 +66,9 @@
z-index: 0; z-index: 0;
overflow: hidden; overflow: hidden;
max-height: 0; max-height: 0;
opacity: 0; /* opacity: 0; */
transition: max-height 0.25s ease-in-out, opacity 0.25s ease-in-out; /* transition: max-height 0.25s ease-in-out, opacity 0.25s ease-in-out; */
transition: max-height ease-in-out 0.25s;
} }
.sidebar hr{ .sidebar hr{
margin-left: 2rem; margin-left: 2rem;
@ -67,7 +76,7 @@
} }
.sidebar-menu-item { .sidebar-menu-item {
margin: 0px 0px 0px -20px; margin: 0px 0px 0px -20px;
padding: 14px 0px 14px calc(35px + 2rem); padding: 15px 0px 15px calc(35px + 2rem);
background-color: var(--dark-blue); background-color: var(--dark-blue);
color: var(--text-primary); color: var(--text-primary);
border-radius: 6px; border-radius: 6px;
@ -102,4 +111,60 @@
padding-left: 35px !important; padding-left: 35px !important;
margin-top: 0px; margin-top: 0px;
margin-bottom: 0px; margin-bottom: 0px;
}
.hamburger{
display: none;
visibility: hidden;
position: absolute;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48px' height='48px' viewBox='0 0 48 48'%3E%3Ctitle%3E70 Basic icons by Xicons.co%3C/title%3E%3Cpath d='M41,14H7a2,2,0,0,1,0-4H41A2,2,0,0,1,41,14Z' fill='%23f5f5f5'/%3E%3Cpath d='M41,26H7a2,2,0,0,1,0-4H41A2,2,0,0,1,41,26Z' fill='%23f5f5f5'/%3E%3Cpath d='M41,38H7a2,2,0,0,1,0-4H41A2,2,0,0,1,41,38Z' fill='%23f5f5f5'/%3E%3C/svg%3E");
background-position: center;
width: 36px;
height: 36px;
right: 30px;
margin-top: calc(0.75rem + 12px);
margin-bottom: calc(1.75rem + 12px);
cursor: pointer;
}
.hamburger:hover{
opacity: 0.8;
}
.log-out-menu-item{
display: none;
visibility: hidden;
}
@media screen and (max-width: 768px){
.page{
padding-left: 15px;
}
.sidebar{
width: 100%;
max-height: 95px;
overflow: hidden;
transition: max-height ease-in-out 0.25s;
}
.sidebar.show-menu{
max-height: 100vh;
transition: max-height ease-in-out 0.25s;
}
.sidebar-logo{
max-height: 60px;
margin-top: 0.75rem;
margin-bottom: 1.75rem;
margin-left: 30px;
max-width: 82vw;
}
.hamburger{
display: block;
visibility: visible;
}
.main-content{
margin-top: 95px;
}
header{
display: none;
}
.log-out-menu-item{
display: flex;
visibility: visible;
}
} }

View File

@ -34,7 +34,9 @@ a:hover:not(.button){
opacity: 1 !important; opacity: 1 !important;
color: #C4C4C4; color: #C4C4C4;
} }
a:hover{
cursor: pointer;
}
details[open] summary ~ * { details[open] summary ~ * {
animation: sweep .15s ease-in-out; animation: sweep .15s ease-in-out;
} }

View File

@ -25,4 +25,14 @@ input{
.registerText{ .registerText{
line-height: 1.6em; line-height: 1.6em;
padding: 18px 10px 4px 10px; padding: 18px 10px 4px 10px;
}
.footer.login{
padding: 15px !important;
position: absolute;
margin: auto;
width: 100%;
bottom: 0;
}
.main-content.login{
margin-top: 0;
} }

View File

@ -35,7 +35,7 @@ const Profile = () => {
return <div className="row"> return <div className="row">
<div className="col-6"> <div className="col-6-lg col-12">
<h3>Profile</h3> <h3>Profile</h3>
@ -48,7 +48,7 @@ const Profile = () => {
</div> </div>
<div className="col-6"> <div className="col-6-lg col-12">
<h3>Settings</h3> <h3>Settings</h3>
<p><b>ID:</b> {user.id}</p> <p><b>ID:</b> {user.id}</p>

View File

@ -1,8 +1,9 @@
import React, { useState } from "react"; import React, { useState, useRef } from "react";
import { Route, Routes, useNavigate } from "react-router"; import { Route, Routes, useNavigate } from "react-router";
import '../css/pages/Login.css'; import '../css/pages/Login.css';
import logoImg from '../img/logo.png'; import logoImg from '../img/logo.png';
import { useLoginContext } from "../structures/UserContext"; import { useLoginContext } from "../structures/UserContext";
import LoadingBar from 'react-top-loading-bar';
import { fetchUser, post } from "../util/Util"; import { fetchUser, post } from "../util/Util";
const loginMethods = [ const loginMethods = [
@ -18,9 +19,10 @@ const CredentialFields = () => {
const [, setUser] = useLoginContext(); const [, setUser] = useLoginContext();
const [fail, setFail] = useState(false); const [fail, setFail] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const ref = useRef(null);
const loginClick = async (event) => { const loginClick = async (event) => {
ref.current.continuousStart();
event.preventDefault(); event.preventDefault();
const username = document.getElementById('username').value; const username = document.getElementById('username').value;
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
@ -29,20 +31,23 @@ const CredentialFields = () => {
const result = await post('/api/login', { username, password }); const result = await post('/api/login', { username, password });
if (![200, 302].includes(result.status)) { if (![200, 302].includes(result.status)) {
ref.current.complete();
setFail(true); setFail(true);
return; return;
} }
if (!result.data?.twoFactor) { if (!result.data?.twoFactor) {
setUser(await fetchUser()); setUser(await fetchUser());
ref.current.complete();
return navigate('/home'); return navigate('/home');
} }
ref.current.complete();
return navigate('verify'); return navigate('verify');
}; };
return <div> return <div>
<LoadingBar color='#2685ff' ref={ref} />
<div className="is-center"> <div className="is-center">
<img className="logoImg mb-4" src={logoImg}></img> <img className="logoImg mb-4" src={logoImg}></img>
</div> </div>
@ -85,24 +90,34 @@ const TwoFactor = () => {
const [fail, setFail] = useState(false); const [fail, setFail] = useState(false);
const [, setUser] = useLoginContext(); const [, setUser] = useLoginContext();
const navigate = useNavigate(); const navigate = useNavigate();
const ref = useRef(null);
const twoFactorClick = async () => { const twoFactorClick = async () => {
const code = document.getElementById('2faCode').value; const code = document.getElementById('2faCode').value;
if (!code) return; if (!code) return;
ref.current.continuousStart();
const result = await post('/api/login/verify', { code }); const result = await post('/api/login/verify', { code });
if (result.status === 200) { if (result.status === 200) {
setUser(await fetchUser()); setUser(await fetchUser());
ref.current.complete();
return navigate('/home'); return navigate('/home');
} }
ref.current.complete();
setFail(true); setFail(true);
}; };
return <div className="card mb-3 is-center dir-column"> return <div>
{fail ? <p>Invalid code</p> : null} <LoadingBar color='#2685ff' ref={ref} />
<h2>Verification code</h2> <div className="is-center">
<input autoComplete='off' placeholder='Code' required id='2faCode' type='password' /> <img className="logoImg mb-4" src={logoImg}></img>
<button onClick={twoFactorClick}>Enter</button> </div>
<div className="card mb-3 is-center dir-column">
<h2>Verification Code</h2>
{fail ? <p className="mt-0">Invalid code</p> : null}
<input autoComplete='off' placeholder='Your 2FA code...' required id='2faCode' type='password' />
<button className="button primary m-3" onClick={twoFactorClick}>Enter</button>
</div>
</div>; </div>;
}; };

View File

@ -1,32 +1,41 @@
import React, { useState } from "react"; import React, { useState, useRef } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { post } from "../util/Util"; import { post } from "../util/Util";
import '../css/pages/Register.css'; import '../css/pages/Register.css';
import logoImg from '../img/logo.png'; import logoImg from '../img/logo.png';
import LoadingBar from 'react-top-loading-bar';
const Register = () => { const Register = () => {
const [error, setError] = useState(); const [error, setError] = useState();
const [params] = useSearchParams(); const [params] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
const ref = useRef(null);
document.body.classList.add('bg-triangles'); document.body.classList.add('bg-triangles');
const code = params.get('code'); const code = params.get('code');
const submit = async (event) => { const submit = async (event) => {
ref.current.continuousStart();
event.preventDefault(); event.preventDefault();
const username = document.getElementById('username').value; const username = document.getElementById('username').value;
const password = document.getElementById('password').value; const password = document.getElementById('password').value;
if (!username.length || !password.length) return; if (!username.length || !password.length) {
ref.current.complete();
return;
}
const response = await post('/api/register', { username, password, code }); const response = await post('/api/register', { username, password, code });
if (response.status !== 200) return setError(response.message); if (response.status !== 200) {
ref.current.complete();
return setError(response.message);
}
ref.current.complete();
navigate('/login'); navigate('/login');
}; };
return <div className="row is-center is-full-screen is-marginless"> return <div className="row is-center is-full-screen is-marginless">
<LoadingBar color='#2685ff' ref={ref} />
<div className='registerWrapper col-6 col-6-md col-3-lg'> <div className='registerWrapper col-6 col-6-md col-3-lg'>
<div> <div>
<div className="is-center"> <div className="is-center">

View File

@ -7496,6 +7496,11 @@ react-scripts@5.0.1:
optionalDependencies: optionalDependencies:
fsevents "^2.3.2" fsevents "^2.3.2"
react-top-loading-bar@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/react-top-loading-bar/-/react-top-loading-bar-2.3.1.tgz#d727eb6aaa412eae52a990e5de9f33e9136ac714"
integrity sha512-rQk2Nm+TOBrM1C4E3e6KwT65iXyRSgBHjCkr2FNja1S51WaPulRA5nKj/xazuQ3x89wDDdGsrqkqy0RBIfd0xg==
react@^18.2.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"