diff --git a/package.json b/package.json index 552a27c..ec70f5c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/index.html b/public/index.html index abce138..b57d19d 100644 --- a/public/index.html +++ b/public/index.html @@ -8,6 +8,7 @@ +
diff --git a/src/api/authenticator.js b/src/api/authenticator.js index e98f807..62a2e97 100644 --- a/src/api/authenticator.js +++ b/src/api/authenticator.js @@ -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()); }; diff --git a/src/components/Modal.js b/src/components/Modal.js new file mode 100644 index 0000000..2704830 --- /dev/null +++ b/src/components/Modal.js @@ -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( + <> + + e.stopPropagation()} + variants={modalAnimation} + initial="hidden" + animate="visible" + exit="exit" + style={{ + width: width, + height: height + }} + > + { (title) && { title } } + {children} + + + , + modalRoot + ); +} \ No newline at end of file diff --git a/src/components/auth/Create.js b/src/components/auth/Create.js index 8fd9612..02d406b 100644 --- a/src/components/auth/Create.js +++ b/src/components/auth/Create.js @@ -68,7 +68,7 @@ export default function Create() {

One more thing!

You need a special code to sign up here!

- +
setSpecialCodeInput(target.value) } /> @@ -84,11 +84,11 @@ export default function Create() { sign up
- +
setUsernameInput(target.value) } />
- +
setPasswordInput(target.value) } />
diff --git a/src/components/auth/Login.js b/src/components/auth/Login.js index 6a0a86d..e6c0105 100644 --- a/src/components/auth/Login.js +++ b/src/components/auth/Login.js @@ -56,11 +56,11 @@ export default function Login() { log in
- +
setUsernameInput(target.value) } />
- +
setPasswordInput(target.value) } />
diff --git a/src/components/channel/ChannelCreateButton.js b/src/components/channel/ChannelCreateButton.js new file mode 100644 index 0000000..8fa81cb --- /dev/null +++ b/src/components/channel/ChannelCreateButton.js @@ -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 = ( + <> +
+ { info } +
+ + ); + } else { + modalContent = ( + <> +
+ + setChannelNameInput(target.value) } /> +
+
+ + +
+ + ); + } + + return <> + + {/* ugly: AnimatePresence is needed to animate the modal closing */} + + {(isDialogOpen) && + { modalContent } + } + + ; +} \ No newline at end of file diff --git a/src/components/channel/ChannelList.js b/src/components/channel/ChannelList.js index 18fec4f..9a9dba1 100644 --- a/src/components/channel/ChannelList.js +++ b/src/components/channel/ChannelList.js @@ -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 (
{ channels.map((channel) => ( )) } +
); } diff --git a/src/components/channel/ChannelMessageView.js b/src/components/channel/ChannelMessageView.js index 8a8f3b2..0403e36 100644 --- a/src/components/channel/ChannelMessageView.js +++ b/src/components/channel/ChannelMessageView.js @@ -38,6 +38,7 @@ export default function ChannelMessageView({ messages, channelId }) { } }; + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(loadOlderMessages, [channelId, dispatch]); useEffect(() => { diff --git a/src/components/main/App.js b/src/components/main/App.js index 3fa7bf0..dc7399d 100644 --- a/src/components/main/App.js +++ b/src/components/main/App.js @@ -45,9 +45,6 @@ function App({ user, fullscreenMessage }) { } /> - } - /> { user && } diff --git a/src/components/main/LoggedInMount.js b/src/components/main/LoggedInMount.js index 17395ef..728c3b3 100644 --- a/src/components/main/LoggedInMount.js +++ b/src/components/main/LoggedInMount.js @@ -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 <>
{ (channelId) && } - { (userId) && }
; } diff --git a/src/components/user/UserButton.js b/src/components/user/UserButton.js index e92c299..75bae00 100644 --- a/src/components/user/UserButton.js +++ b/src/components/user/UserButton.js @@ -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 ( - + <> + + {(isPromptOpen) && setIsPromptOpen(false) } alignItems="center" width="150px" height="150px"> + + { user.status === 1 ? "Online" : "Offline" } + { userObject ? userObject.role.toLowerCase() : "loading..." } + } + + + ); } \ No newline at end of file diff --git a/src/components/user/UserView.js b/src/components/user/UserView.js deleted file mode 100644 index 452227a..0000000 --- a/src/components/user/UserView.js +++ /dev/null @@ -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 = <> - - {(userObject.role === "ADMIN") && Admin} - {(userObject.role === "USER") && User} - - } else { - view = <> - - - } - - return ( -
-
- {view} -
-
- ); -} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 0768827..b57c6af 100644 --- a/src/index.js +++ b/src/index.js @@ -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( - + + + , document.getElementById('root') diff --git a/src/styles/App.scss b/src/styles/App.scss index 2e0d31b..1435a17 100644 --- a/src/styles/App.scss +++ b/src/styles/App.scss @@ -35,6 +35,11 @@ body { max-height: 100vh; margin: 0px; padding: 0px; + overflow: hidden; +} + +#modal-root { + overflow: hidden; } button, input, optgroup, select, textarea { diff --git a/src/styles/Components/Containers.scss b/src/styles/Components/Containers.scss index 3c2c85e..c342900 100644 --- a/src/styles/Components/Containers.scss +++ b/src/styles/Components/Containers.scss @@ -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; +} \ No newline at end of file diff --git a/src/styles/Components/ProfileLink.scss b/src/styles/Components/ProfileLink.scss index 1bcda17..ceee7a0 100644 --- a/src/styles/Components/ProfileLink.scss +++ b/src/styles/Components/ProfileLink.scss @@ -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 { diff --git a/src/styles/Components/Textbox.scss b/src/styles/Components/Textbox.scss index ca8a5e3..21962a4 100644 --- a/src/styles/Components/Textbox.scss +++ b/src/styles/Components/Textbox.scss @@ -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; } } \ No newline at end of file diff --git a/src/styles/Components/properties.scss b/src/styles/Components/properties.scss index 5692fa9..f69cfd6 100644 --- a/src/styles/Components/properties.scss +++ b/src/styles/Components/properties.scss @@ -4,4 +4,10 @@ .elevated-2 { box-shadow: rgba(0, 0, 0, 0.50) 0px 25px 50px -4px; -} \ No newline at end of file +} + +.label { + text-transform: uppercase; + font-weight: 600; + color: var(--darker-text-color); +} diff --git a/src/styles/root.scss b/src/styles/root.scss index 054d90c..6663995 100644 --- a/src/styles/root.scss +++ b/src/styles/root.scss @@ -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); diff --git a/yarn.lock b/yarn.lock index c7543e0..6830d3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==