feat: modal system, "create channel" button

This commit is contained in:
hippoz 2021-10-21 22:14:55 +03:00
parent ec4c98b760
commit 3c216557db
Signed by: hippoz
GPG key ID: 7C52899193467641
21 changed files with 367 additions and 72 deletions

View file

@ -3,9 +3,11 @@
"version": "0.1.0",
"private": false,
"dependencies": {
"framer-motion": "^4.1.17",
"react": "^17.0.2",
"react-content-loader": "^6.0.3",
"react-dom": "^17.0.2",
"react-feather": "^2.0.9",
"react-media-hook": "^0.4.9",
"react-redux": "^7.2.5",
"react-router-dom": "^5.3.0",

View file

@ -8,6 +8,7 @@
<body>
<noscript>Sorry, but JavaScript is required.</noscript>
<div id="modal-root"></div>
<div id="root"></div>
</body>
</html>

View file

@ -6,5 +6,9 @@ const { log: authLog } = Logger([ 'Authenticator' ]);
export function login() {
authLog('Logging in through gateway...');
if (gateway.handshakeCompleted) {
authLog("Gateway connection already exists, tearing down existing one...");
gateway.ws.close();
}
return gateway.connect(getToken());
};

58
src/components/Modal.js Normal file
View file

@ -0,0 +1,58 @@
import { m } from "framer-motion";
import { createPortal } from "react-dom";
const modalRoot = document.getElementById('modal-root');
const modalAnimation = {
hidden: {
opacity: 0,
scale: 0.8,
},
visible: {
opacity: 1,
scale: 1,
transition: {
duration: 0.1,
ease: "easeIn",
},
},
exit: {
opacity: 0,
scale: 0.8,
transition: {
duration: 0.1,
ease: "easeOut",
},
},
};
export default function Modal({ width=600, height=400, alignItems="default", title, children, onClose }) {
return createPortal(
<>
<m.div
className="backdrop"
onClick={onClose}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<m.div
className={ alignItems === "center" ? "modal-centered" : "modal" }
onClick={(e) => e.stopPropagation()}
variants={modalAnimation}
initial="hidden"
animate="visible"
exit="exit"
style={{
width: width,
height: height
}}
>
{ (title) && <span className="modal-title">{ title }</span> }
{children}
</m.div>
</m.div>
</>,
modalRoot
);
}

View file

@ -68,7 +68,7 @@ export default function Create() {
<div id="login-container">
<h1>One more thing!</h1>
<p>You need a special code to sign up here!</p>
<label htmlFor="specialcode">Special Code</label>
<label htmlFor="specialcode" className="label">Special Code</label>
<br />
<input type="password" name="specialcode" className="text-input" onChange={ ({ target }) => setSpecialCodeInput(target.value) } />
<button id="login-submit" className="button" onClick={ doCreateAccount }>Continue</button>
@ -84,11 +84,11 @@ export default function Create() {
<span className="greeter-branding-name">sign up</span>
<div className="center">
<div id="login-container">
<label htmlFor="username">Username</label>
<label htmlFor="username" className="label">Username</label>
<br />
<input type="text" name="username" className="text-input" onChange={ ({ target }) => setUsernameInput(target.value) } />
<br />
<label htmlFor="password">Password</label>
<label htmlFor="password" className="label">Password</label>
<br />
<input type="password" name="password" className="text-input" onChange={ ({ target }) => setPasswordInput(target.value) } />
<br />

View file

@ -56,11 +56,11 @@ export default function Login() {
<span className="greeter-branding-name">log in</span>
<div className="center">
<div id="login-container">
<label htmlFor="username">Username</label>
<label htmlFor="username" className="label">Username</label>
<br />
<input type="text" name="username" className="text-input" onChange={ ({ target }) => setUsernameInput(target.value) } />
<br />
<label htmlFor="password">Password</label>
<label htmlFor="password" className="label">Password</label>
<br />
<input type="password" name="password" className="text-input" onChange={ ({ target }) => setPasswordInput(target.value) } />
<br />

View file

@ -0,0 +1,92 @@
import { AnimatePresence } from "framer-motion";
import { useState } from "react";
import Modal from "../Modal";
import { authenticated } from '../../api/request';
import { getCreateChannelError } from "../../common/util/errors";
import { login } from "../../api/authenticator";
export default function ChannelCreateButton() {
const [ isDialogOpen, setIsDialogOpen ] = useState(false);
const [ channelNameInput, setChannelNameInput ] = useState();
const [ info, setInfo ] = useState(null);
const handleClose = () => {
setIsDialogOpen(false);
setInfo(null);
};
const createChannel = async () => {
setInfo("creating channel...");
const { json, isOK } = await authenticated('/api/v1/content/channel/create', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: channelNameInput
})
});
if (!isOK && json) {
setInfo(getCreateChannelError(json));
return;
}
if (!isOK) {
setInfo("Something went wrong");
return;
}
login(); // ugly: we need to relog in order to see the new channel
handleClose();
};
let modalContent;
if (info) {
modalContent = (
<>
<div className="center grow col-flex">
<span className="greeter-branding-name">{ info }</span>
</div>
</>
);
} else {
modalContent = (
<>
<div className="input-group">
<label htmlFor="channel-name" className="label" style={{ float: "left" }}>Channel Name</label>
<input type="text" name="channel-name" className="text-input" onChange={ ({ target }) => setChannelNameInput(target.value) } />
</div>
<div className="full-width">
<button style={{ float: "right" }} id="login-submit" className="button-pressed" onClick={ createChannel }>Create</button>
<button style={{ float: "left" }} className="button" onClick={ handleClose }>Cancel</button>
</div>
</>
);
}
return <>
<button className="button button-channel" onClick={() => setIsDialogOpen(true)}>
<div className="profile-link">
<div className="profile-picture add-channel" alt="Profile">
<span className="default-channel-styled-text">+</span>
</div>
<span className="profile-username">New Channel</span>
</div>
</button>
{/* ugly: AnimatePresence is needed to animate the modal closing */}
<AnimatePresence>
{(isDialogOpen) && <Modal
width="500px"
height="300px"
title="Create a channel"
alignItems="center"
onClose={ handleClose }
>
{ modalContent }
</Modal>}
</AnimatePresence>
</>;
}

View file

@ -2,6 +2,7 @@ import { connect } from 'react-redux'
import ChannelListLoader from './ChannelListLoader';
import ChannelButton from './ChannelButton';
import ChannelCreateButton from './ChannelCreateButton';
function ChannelList({ selectedChannelId, channels }) {
if (!channels) {
@ -14,6 +15,7 @@ function ChannelList({ selectedChannelId, channels }) {
return (
<div className="channel-list">
{ channels.map((channel) => ( <ChannelButton key={ channel._id } channel={ channel } selected={ (channel._id === selectedChannelId) } /> )) }
<ChannelCreateButton />
</div>
);
}

View file

@ -38,6 +38,7 @@ export default function ChannelMessageView({ messages, channelId }) {
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(loadOlderMessages, [channelId, dispatch]);
useEffect(() => {

View file

@ -45,9 +45,6 @@ function App({ user, fullscreenMessage }) {
<Route path="/channels/:channelId"
render={() => <LoggedInMount />}
/>
<Route path="/user/:userId"
render={() => <LoggedInMount />}
/>
<Route path="/">
{ user && <LoggedInMount /> }

View file

@ -4,16 +4,14 @@ import { useParams } from "react-router-dom";
import Sidebar from "../Sidebar";
import ChannelView from "../channel/ChannelView";
import GradientBanner from "../GradientBanner";
import UserView from "../user/UserView";
function LoggedInMount({ gradientBannerNotificationText }) {
const { channelId, userId } = useParams();
const { channelId } = useParams();
return <>
<Sidebar />
<div className="col-flex">
<GradientBanner text={ gradientBannerNotificationText }/>
{ (channelId) && <ChannelView channelId={ channelId } /> }
{ (userId) && <UserView userId={ userId } /> }
</div>
</>;
}

View file

@ -1,17 +1,39 @@
import UserProfile from './UserProfileLink';
import { useState, useEffect } from "react";
import { useHistory } from 'react-router-dom';
import UserProfile from "./UserProfileLink";
import { authenticated } from "../../api/request";
import Modal from "../Modal";
import { AnimatePresence } from "framer-motion";
export default function ChannelUserButton({ user, subtext }) {
const history = useHistory();
const [ isPromptOpen, setIsPromptOpen ] = useState(false);
const [userObject, setUserObject] = useState(null);
const handleClick = () => {
history.push(`/user/${user._id}`);
};
useEffect(() => {
authenticated(`/api/v1/users/user/${user._id}/info`, {
method: 'GET',
headers: {
"Accept": "application/json"
}
}).then(({ isOK, json }) => {
if (isOK) {
setUserObject(json.user);
}
});
}, [user]);
return (
<button className="button button-channel" onClick={ handleClick }>
<UserProfile subtext={ subtext } user={ user } size="32" />
</button>
<>
<AnimatePresence>
{(isPromptOpen) && <Modal onClose={ () => setIsPromptOpen(false) } alignItems="center" width="150px" height="150px">
<UserProfile subtext={ subtext } user={ user } size="128" />
<span className="label">{ user.status === 1 ? "Online" : "Offline" }</span>
<span className="label">{ userObject ? userObject.role.toLowerCase() : "loading..." }</span>
</Modal>}
</AnimatePresence>
<button className="button button-channel" onClick={ () => setIsPromptOpen(true) }>
<UserProfile subtext={ subtext } user={ user } size="32" />
</button>
</>
);
}

View file

@ -1,42 +0,0 @@
import { useEffect, useState } from "react";
import { authenticated } from "../../api/request";
import UserProfile from "./UserProfileLink";
import ProfileLinkLoader from "../ProfileLinkLoader";
export default function UserView({ userId }) {
const [userObject, setUserObject] = useState(null);
useEffect(() => {
authenticated(`/api/v1/users/user/${userId}/info`, {
method: 'GET',
headers: {
"Accept": "application/json"
}
}).then(({ isOK, json }) => {
if (isOK) {
setUserObject(json.user);
}
});
}, [userId]);
let view = null;
if (userObject) {
view = <>
<UserProfile user={ userObject } size="32" />
{(userObject.role === "ADMIN") && <span style={{ padding: "12px" }}>Admin</span>}
{(userObject.role === "USER") && <span style={{ padding: "12px" }}>User</span>}
</>
} else {
view = <>
<ProfileLinkLoader />
</>
}
return (
<div className="center grow">
<div className="user-view center">
{view}
</div>
</div>
);
}

View file

@ -4,11 +4,14 @@ import App from './components/main/App';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { LazyMotion, domAnimation } from "framer-motion"
ReactDOM.render(
<React.StrictMode>
<Provider store={ store }>
<App />
<LazyMotion features={ domAnimation }>
<App />
</LazyMotion>
</Provider>
</React.StrictMode>,
document.getElementById('root')

View file

@ -35,6 +35,11 @@ body {
max-height: 100vh;
margin: 0px;
padding: 0px;
overflow: hidden;
}
#modal-root {
overflow: hidden;
}
button, input, optgroup, select, textarea {

View file

@ -62,6 +62,14 @@
flex-grow: 1;
}
.full-width {
width: 100%;
}
.full-height {
height: 100%;
}
.profile-badge {
margin: 16px;
}
@ -87,3 +95,68 @@
font-size: 3em;
padding-bottom: 18px;
}
.modal {
min-width: 100px;
min-height: 100px;
max-width: 50%;
max-height: 50%;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
padding: 1.5em;
border-radius: 1em;
background-color: var(--accent-color-dark);
overflow: hidden;
}
@media screen and (max-width: 768px) {
.modal {
/* !important is used here because the height and width can be set using inline styles in the Modal component */
height: 100% !important;
width: 100% !important;
max-height: 100%;
max-width: 100%;
}
}
.modal-centered {
@extend .modal;
display: flex;
align-items: center;
text-align: center;
flex-direction: column;
overflow: hidden;
}
.modal-title {
font-size: 1.74rem;
font-weight: 700;
}
.backdrop {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
background: #0000008f;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999999999;
overflow: hidden;
}
.input-group {
flex-grow: 1;
display: flex;
width: 100%;
flex-direction: column;
margin: 32px;
text-align: initial;
}

View file

@ -14,10 +14,13 @@
color: var(--default-text-color);
}
&.no-messages-icon {
border-radius: 0;
width: 16em;
height: 16em;
&.add-channel {
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: var(--create-channel-background);
color: var(--default-text-color);
}
&.default-user {

View file

@ -1,11 +1,11 @@
.text-input {
margin: 6px;
padding: 6px;
padding: 14px;
border: none;
min-width: 220px;
color: var(--default-text-color);
border-radius: var(--default-button-border-radius);
background-color: var(--message-box-color);
flex-grow: 1;
&.message-input {
border-radius: var(--message-box-border-radius);
@ -16,5 +16,6 @@
padding-left: 16px;
margin-top: 6px;
font-size: 16px;
flex-grow: 1;
}
}

View file

@ -4,4 +4,10 @@
.elevated-2 {
box-shadow: rgba(0, 0, 0, 0.50) 0px 25px 50px -4px;
}
}
.label {
text-transform: uppercase;
font-weight: 600;
color: var(--darker-text-color);
}

View file

@ -21,6 +21,13 @@
hsl(225, 35%, 40%)
);
--create-channel-background: linear-gradient(
to top right,
hsl(75, 35%, 40%),
hsl(150, 35%, 40%),
hsl(200, 35%, 40%)
);
--default-scrollbar-color: var(--accent-color);
--default-scrollbar-color-track: var(--background-color);
--default-scrollbar-width: 1px;
@ -28,7 +35,7 @@
--channel-top-bar-color-accent: var(--background-color);
--channel-top-bar-color: var(--background-color);
--sidebar-background-color: hsl(230, 12%, 12%);
--elevation-box-shadow: 0 1px 0 0 hsla(230, 12%, 8%, 0.2), 0 2px 0 0 hsla(230, 12%, 8%, 0.2), 0 3px 0 0 hsla(230, 12%, 10%, 0.1);
--elevation-box-shadow: 0 2px 0 0 hsla(230, 12%, 8%, 0.167), 0 2px 0 0 hsla(230, 12%, 8%, 0.08), 0 3px 0 0 hsla(230, 12%, 10%, 0.07);
--message-box-color: var(--accent-color);
--button-color: var(--accent-color-dark);

View file

@ -1219,6 +1219,18 @@
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18"
integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==
"@emotion/is-prop-valid@^0.8.2":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
dependencies:
"@emotion/memoize" "0.7.4"
"@emotion/memoize@0.7.4":
version "0.7.4"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@eslint/eslintrc@^0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
@ -5061,6 +5073,26 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
framer-motion@^4.1.17:
version "4.1.17"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.17.tgz#4029469252a62ea599902e5a92b537120cc89721"
integrity sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==
dependencies:
framesync "5.3.0"
hey-listen "^1.0.8"
popmotion "9.3.6"
style-value-types "4.1.4"
tslib "^2.1.0"
optionalDependencies:
"@emotion/is-prop-valid" "^0.8.2"
framesync@5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.3.0.tgz#0ecfc955e8f5a6ddc8fdb0cc024070947e1a0d9b"
integrity sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==
dependencies:
tslib "^2.1.0"
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@ -5413,6 +5445,11 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
@ -8036,6 +8073,16 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
popmotion@9.3.6:
version "9.3.6"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.6.tgz#b5236fa28f242aff3871b9e23721f093133248d1"
integrity sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==
dependencies:
framesync "5.3.0"
hey-listen "^1.0.8"
style-value-types "4.1.4"
tslib "^2.1.0"
portfinder@^1.0.26:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
@ -9017,6 +9064,13 @@ react-error-overlay@^6.0.9:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
react-feather@^2.0.9:
version "2.0.9"
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.9.tgz#6e42072130d2fa9a09d4476b0e61b0ed17814480"
integrity sha512-yMfCGRkZdXwIs23Zw/zIWCJO3m3tlaUvtHiXlW+3FH7cIT6fiK1iJ7RJWugXq7Fso8ZaQyUm92/GOOHXvkiVUw==
dependencies:
prop-types "^15.7.2"
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -10267,6 +10321,14 @@ style-loader@1.3.0:
loader-utils "^2.0.0"
schema-utils "^2.7.0"
style-value-types@4.1.4:
version "4.1.4"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-4.1.4.tgz#80f37cb4fb024d6394087403dfb275e8bb627e75"
integrity sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==
dependencies:
hey-listen "^1.0.8"
tslib "^2.1.0"
stylehacks@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
@ -10580,7 +10642,7 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.3:
tslib@^2.0.3, tslib@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==