diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..cceed8d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## Summary + +## New Features + +## Updates + +## Additional Info + +- [ ] Does this update require migration? (If yes, add extra details) +- [ ] Are there any other PRs that need to be merged? (If yes, add extra details) diff --git a/app/app.jsx b/app/app.jsx index 677bfc9..70f0ad8 100644 --- a/app/app.jsx +++ b/app/app.jsx @@ -44,9 +44,7 @@ const App = () => { if (page === 'settings') { return ( - - - + ); } diff --git a/app/assets/react.svg b/app/assets/react.svg deleted file mode 100644 index 7b17ad7..0000000 --- a/app/assets/react.svg +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/app/components/icons/faCircleCheck.jsx b/app/components/icons/faCircleCheck.jsx new file mode 100644 index 0000000..fb546b7 --- /dev/null +++ b/app/components/icons/faCircleCheck.jsx @@ -0,0 +1,30 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const FaCircleCheck = ({ height, width, checkColor = 'white' }) => ( + + + + +); + +FaCircleCheck.displayName = 'FaCircleCheck'; + +FaCircleCheck.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + checkColor: PropTypes.string, +}; + +export default FaCircleCheck; diff --git a/app/components/icons/faUserCircle.jsx b/app/components/icons/faUserCircle.jsx new file mode 100644 index 0000000..da54c56 --- /dev/null +++ b/app/components/icons/faUserCircle.jsx @@ -0,0 +1,25 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const FaUserCircle = ({ width = 25, height = 25 }) => ( + + + +); + +FaUserCircle.displayName = 'FaUserCircle'; + +FaUserCircle.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +export default FaUserCircle; diff --git a/app/components/icons/faUsers.jsx b/app/components/icons/faUsers.jsx new file mode 100644 index 0000000..7dead35 --- /dev/null +++ b/app/components/icons/faUsers.jsx @@ -0,0 +1,25 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const FaUsers = ({ height = 25, width = 25 }) => ( + + + +); + +FaUsers.displayName = 'FaUsers'; + +FaUsers.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +export default FaUsers; diff --git a/app/components/icons/index.jsx b/app/components/icons/index.jsx index 5040a49..c2f6d61 100644 --- a/app/components/icons/index.jsx +++ b/app/components/icons/index.jsx @@ -1,6 +1,11 @@ import BsTable from './bsTable'; import FaCheck from './faCheck'; +import FaChevronDown from './faChevronDown'; +import FaChevronLeft from './faChevronLeft'; +import FaChevronRight from './faChevronRight'; import FaChevronUp from './faChevronUp'; +import FaCircleCheck from './faCircleCheck'; +import FaUserCircle from './faUserCircle'; import FaClose from './faClose'; import FaCog from './faCog'; import FaEllipsisVertical from './faEllipsisVertical'; @@ -12,15 +17,27 @@ import FaTrash from './faTrash'; import FiLayout from './fiLayout'; import HiStatusOffline from './hiStatusOffline'; import HiStatusOnline from './hiStatusOnline'; +import IoArrowBack from './ioArrowBack'; +import IoColorPalette from './ioColorPalette'; import IoGrid from './ioGrid'; +import LiaSyncSolid from './liaSyncSolid'; import MdEdit from './mdEdit'; +import MdErrorOutline from './mdErrorOutline'; +import MdEye from './mdEye'; +import MdEyeOff from './mdEyeOff'; +import MdHelpCircle from './mdHelpCircle'; import PiListFill from './piListFill'; import StatusLogo from './statusLogo'; export { BsTable, FaCheck, + FaChevronDown, + FaChevronLeft, + FaChevronRight, + FaCircleCheck, FaChevronUp, + FaUserCircle, FaClose, FaCog, FaEllipsisVertical, @@ -32,8 +49,15 @@ export { FiLayout, HiStatusOffline, HiStatusOnline, + IoArrowBack, + IoColorPalette, IoGrid, + LiaSyncSolid, MdEdit, + MdErrorOutline, + MdEye, + MdEyeOff, + MdHelpCircle, PiListFill, StatusLogo, }; diff --git a/app/components/icons/ioArrowBack.jsx b/app/components/icons/ioArrowBack.jsx new file mode 100644 index 0000000..f4e4196 --- /dev/null +++ b/app/components/icons/ioArrowBack.jsx @@ -0,0 +1,31 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const IoArrowBack = ({ height = 25, width = 25 }) => ( + + + +); + +IoArrowBack.displayName = 'IoArrowBack'; + +IoArrowBack.propTypes = { + height: PropTypes.number, + width: PropTypes.number, +}; + +export default IoArrowBack; diff --git a/app/components/icons/ioColorPalette.jsx b/app/components/icons/ioColorPalette.jsx new file mode 100644 index 0000000..bf65395 --- /dev/null +++ b/app/components/icons/ioColorPalette.jsx @@ -0,0 +1,25 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const IoColorPalette = ({ height = 25, width = 25 }) => ( + + + +); + +IoColorPalette.displayName = 'IoColorPalette'; + +IoColorPalette.propTypes = { + height: PropTypes.number, + width: PropTypes.number, +}; + +export default IoColorPalette; diff --git a/app/components/icons/liaSyncSolid.jsx b/app/components/icons/liaSyncSolid.jsx new file mode 100644 index 0000000..0703941 --- /dev/null +++ b/app/components/icons/liaSyncSolid.jsx @@ -0,0 +1,25 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const LiaSyncSolid = ({ height, width }) => ( + + + +); + +LiaSyncSolid.displayName = 'LiaSyncSolid'; + +LiaSyncSolid.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +export default LiaSyncSolid; diff --git a/app/components/icons/mdEye.jsx b/app/components/icons/mdEye.jsx new file mode 100644 index 0000000..d62331f --- /dev/null +++ b/app/components/icons/mdEye.jsx @@ -0,0 +1,25 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const MdEye = ({ width = 25, height = 25 }) => ( + + + +); + +MdEye.displayName = 'MdEye'; + +MdEye.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +export default MdEye; diff --git a/app/components/icons/mdEyeOff.jsx b/app/components/icons/mdEyeOff.jsx new file mode 100644 index 0000000..e94def6 --- /dev/null +++ b/app/components/icons/mdEyeOff.jsx @@ -0,0 +1,25 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const MdEyeOff = ({ width = 25, height = 25 }) => ( + + + +); + +MdEyeOff.displayName = 'MdEyeOff'; + +MdEyeOff.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +export default MdEyeOff; diff --git a/app/components/icons/mdHelpCircle.jsx b/app/components/icons/mdHelpCircle.jsx new file mode 100644 index 0000000..a3a46c4 --- /dev/null +++ b/app/components/icons/mdHelpCircle.jsx @@ -0,0 +1,25 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const MdHelpCircle = ({ width = 24, height = 24 }) => ( + + + +); + +MdHelpCircle.displayName = 'MdHelpCircle'; + +MdHelpCircle.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +export default MdHelpCircle; diff --git a/app/components/modal/monitor/pages/http/statusCodes.jsx b/app/components/modal/monitor/pages/http/statusCodes.jsx index e7fa9fd..75c35e1 100644 --- a/app/components/modal/monitor/pages/http/statusCodes.jsx +++ b/app/components/modal/monitor/pages/http/statusCodes.jsx @@ -17,6 +17,8 @@ const MonitorHttpStatusCodes = ({ code.includes(selectSearch) ); + const monitorStatusCodes = Array.isArray(selectedIds) ? selectedIds : []; + return ( <> @@ -26,7 +28,7 @@ const MonitorHttpStatusCodes = ({ isOpen={selectIsOpen} toggleSelect={toggleSelect} > - {selectedIds.join(', ')} + {monitorStatusCodes?.join(', ')} handleStatusCodeSelect(code)} showDot - isSelected={selectedIds.includes(code)} + isSelected={monitorStatusCodes.includes(code)} > {code} diff --git a/app/components/modal/settings/account/avatar.jsx b/app/components/modal/settings/account/avatar.jsx new file mode 100644 index 0000000..be02cac --- /dev/null +++ b/app/components/modal/settings/account/avatar.jsx @@ -0,0 +1,100 @@ +import './avatar.scss'; + +// import dependencies +import PropTypes from 'prop-types'; +import { useState } from 'react'; + +// import local files +import Modal from '../../../ui/modal'; +import TextInput from '../../../ui/input'; + +const avatars = [ + 'Ape', + 'Bear', + 'Cat', + 'Dog', + 'Duck', + 'Eagle', + 'Fox', + 'Gerbil', + 'Hamster', + 'Hedgehog', + 'Koala', + 'Ostrich', + 'Panda', + 'Rabbit', + 'Rocket', + 'Smart-Dog', + 'Tiger', +]; + +const isImageUrl = (url) => { + if (typeof url !== 'string') { + return false; + } + + return url.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif)$/gim); +}; + +const SettingsAccountAvatarModal = ({ + value = 'Panda', + closeModal, + handleSumbit, +}) => { + const [avatar, setAvatar] = useState(value); + + const isUrl = isImageUrl(avatar); + const imageUrl = isUrl ? avatar : `/icons/${avatar}.png`; + + return ( + <> + Change Avatar + +
+ +
+ setAvatar(e.target.value)} + /> + +
Or select from below
+
+ {avatars.map((avatarName) => ( + {avatarName} setAvatar(avatarName)} + /> + ))} +
+
+ + + + Cancel + + handleSumbit(avatar)}> + Update + + + + ); +}; + +SettingsAccountAvatarModal.displayName = 'SettingsAccountAvatarModal'; + +SettingsAccountAvatarModal.propTypes = { + value: PropTypes.string.isRequired, + closeModal: PropTypes.func.isRequired, + handleSumbit: PropTypes.func.isRequired, +}; + +export default SettingsAccountAvatarModal; diff --git a/app/components/modal/settings/account/avatar.scss b/app/components/modal/settings/account/avatar.scss new file mode 100644 index 0000000..0d3dc8d --- /dev/null +++ b/app/components/modal/settings/account/avatar.scss @@ -0,0 +1,36 @@ +@import '../../../../styles/global.scss'; + +.settings-modal-avatar-container { + display: flex; + justify-content: center; + align-items: center; +} + +.settings-modal-avatar-image { + width: pxToRem(100); + height: pxToRem(100); + border-radius: var(--radius-pill); +} + +.settings-modal-avatars-container { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.settings-modal-avatar-option { + width: pxToRem(60); + height: pxToRem(60); + border: pxToRem(2) solid #00000000; + + &:hover { + border: pxToRem(2) solid var(--primary-600); + cursor: pointer; + } +} + +.settings-modal-avatar-option-select { + width: pxToRem(60); + height: pxToRem(60); + border: pxToRem(2) solid var(--primary-600); +} diff --git a/app/components/modal/settings/account/delete.jsx b/app/components/modal/settings/account/delete.jsx new file mode 100644 index 0000000..5e510cd --- /dev/null +++ b/app/components/modal/settings/account/delete.jsx @@ -0,0 +1,97 @@ +import './avatar.scss'; + +// import dependencies +import PropTypes from 'prop-types'; +import { toast } from 'sonner'; +import { useNavigate } from 'react-router-dom'; + +// import local files +import Modal from '../../../ui/modal'; +import TextInput from '../../../ui/input'; + +import { AlertError } from '../../../ui/alert'; +import { createPostRequest } from '../../../../services/axios'; + +const SettingsAccountDeleteModal = ({ closeModal }) => { + const navigate = useNavigate(); + + const handleDeleteAccount = async () => { + try { + const transferConfirm = document.getElementById( + 'settings-transfer-confirm' + ).value; + + if (transferConfirm.toLowerCase().trim() !== 'delete account') { + toast.error('Enter delete account to confirm.'); + return; + } + + const query = await createPostRequest('/api/user/delete/account'); + + if (query.status === 200) { + toast.success('Account as been deleted!'); + closeModal(); + return navigate('/login'); + } + + toast.error('Something went wrong, please try again later.'); + } catch (error) { + if (error.response?.status === 403) { + return toast.error(error.response.data); + } + + toast.error('Something went wrong, please try again later.'); + } + }; + + return ( + <> + Delete Account + + + + + To verify, type{' '} + delete account below: + + } + /> + +
+ By continuing, your account will be deleted, along with any access you + have to data. +
+
+ + + + Cancel + + + Delete account + + + + ); +}; + +SettingsAccountDeleteModal.displayName = 'SettingsAccountDeleteModal'; + +SettingsAccountDeleteModal.propTypes = { + closeModal: PropTypes.func.isRequired, +}; + +export default SettingsAccountDeleteModal; diff --git a/app/components/modal/settings/account/password.jsx b/app/components/modal/settings/account/password.jsx new file mode 100644 index 0000000..a037fc2 --- /dev/null +++ b/app/components/modal/settings/account/password.jsx @@ -0,0 +1,135 @@ +// import dependencies +import PropTypes from 'prop-types'; + +// import local files +import Modal from '../../../ui/modal'; +import TextInput from '../../../ui/input'; +import RegisterChecklist from '../../../register/checklist'; +import { useState } from 'react'; +import { MdEye, MdEyeOff } from '../../../icons'; +import handleChangePassword from '../../../../handlers/settings/account/password'; +import validators from '../../../../../server/utils/validators'; + +const SettingsAccountPasswordModal = ({ modalTitle, id, closeModal }) => { + const [values, setValues] = useState({ + current: '', + new: '', + repeat: '', + showPassword: false, + showNewPassword: false, + errors: {}, + }); + + const handleOnBlur = (key, value) => { + const isInvalidPassword = validators.auth.password(value); + handleErrors(key, isInvalidPassword?.password || null); + }; + + const handlePasswordChange = (key, value) => { + setValues((prev) => ({ + ...prev, + [key]: value, + })); + }; + + const handleErrors = (key, error) => { + setValues((prev) => ({ + ...prev, + errors: { + ...prev.errors, + [key]: error, + }, + })); + }; + + const submit = async () => { + const { + current: currentPassword, + new: newPassword, + repeat: repeatPassword, + } = values; + + await handleChangePassword({ + currentPassword, + newPassword, + repeatPassword, + handleErrors, + closeModal, + }); + }; + + return ( + <> + {modalTitle} + + handlePasswordChange('current', e.target.value)} + onBlur={(e) => handleOnBlur('current', e.target.value)} + type={values.showPassword ? 'text' : 'password'} + iconRight={ +
+ handlePasswordChange('showPassword', !values.showPassword) + } + > + {values.showPassword ? : } +
+ } + error={values.errors.current} + /> + handlePasswordChange('new', e.target.value)} + onBlur={(e) => handleOnBlur('new', e.target.value)} + type={values.showNewPassword ? 'text' : 'password'} + value={values.new} + iconRight={ +
+ handlePasswordChange('showNewPassword', !values.showNewPassword) + } + > + {values.showNewPassword ? : } +
+ } + error={values.errors.new} + /> + + + + handlePasswordChange('repeat', e.target.value)} + onBlur={(e) => handleOnBlur('repeat', e.target.value)} + type={'password'} + value={values.repeat} + error={values.errors.repeat} + /> +
+ + + + Cancel + + + Update + + + + ); +}; + +SettingsAccountPasswordModal.displayName = 'SettingsAccountPasswordModal'; + +SettingsAccountPasswordModal.propTypes = { + modalTitle: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + closeModal: PropTypes.func.isRequired, +}; + +export default SettingsAccountPasswordModal; diff --git a/app/components/modal/settings/account/transfer.jsx b/app/components/modal/settings/account/transfer.jsx new file mode 100644 index 0000000..2c966df --- /dev/null +++ b/app/components/modal/settings/account/transfer.jsx @@ -0,0 +1,161 @@ +import './avatar.scss'; + +// import dependencies +import PropTypes from 'prop-types'; +import { useEffect } from 'react'; +import { observer } from 'mobx-react-lite'; +import { toast } from 'sonner'; + +// import local files +import Modal from '../../../ui/modal'; +import TextInput from '../../../ui/input'; +import useTeamContext from '../../../../context/team'; +import { createGetRequest } from '../../../../services/axios'; +import useDropdown from '../../../../hooks/useDropdown'; +import Dropdown from '../../../ui/dropdown'; +import { AlertError } from '../../../ui/alert'; +import useContextStore from '../../../../context'; +import handleTransferAccount from '../../../../handlers/settings/account/transfer'; + +const SettingsAccountTransferModal = ({ closeModal }) => { + const { getTeam, setTeam } = useTeamContext(); + const { + userStore: { user }, + } = useContextStore(); + const { dropdownIsOpen, selectedId, toggleDropdown, handleDropdownSelect } = + useDropdown(); + const team = getTeam(); + + const sortedMembers = team?.sort((a, b) => a?.permission - b?.permission).filter((member) => member.isVerified); + + useEffect(() => { + const fetchTeam = async () => { + try { + const query = await createGetRequest('/api/user/team'); + + const filteredMembers = query.data?.filter( + (member) => member.email !== user.email + ); + + setTeam(filteredMembers); + } catch (error) { + console.log(error); + toast.error("Couldn't fetch team members"); + } + }; + + fetchTeam(); + }, []); + + const dropdownItems = sortedMembers.map((member) => ( + { + handleDropdownSelect(member.email); + toggleDropdown(); + }} + showDot + isSelected={selectedId === member.email} + dotColor="primary" + > + {member.email} + + )); + + return ( + <> + + Transfer ownership + + + + +
+ Select member to transfer ownership +
+ + + + {selectedId || 'Select member'} + + + {dropdownItems} + + + + + To verify, type{' '} + transfer ownership{' '} + below: + + } + /> + +
+ By continuing, you acknowledge that the application will be + transferred to the selected user. +
+
+ + + + Cancel + + { + const transferConfirm = document.getElementById( + 'settings-transfer-confirm' + ).value; + + if (transferConfirm.toLowerCase().trim() !== 'transfer ownership') { + return toast.error('Enter transfer ownership to confirm.'); + } + + handleTransferAccount(selectedId, closeModal); + }} + > + Confirm + + + + ); +}; + +SettingsAccountTransferModal.displayName = 'SettingsAccountTransferModal'; + +SettingsAccountTransferModal.propTypes = { + closeModal: PropTypes.func.isRequired, +}; + +export default observer(SettingsAccountTransferModal); diff --git a/app/components/modal/settings/account/username.jsx b/app/components/modal/settings/account/username.jsx new file mode 100644 index 0000000..9253b04 --- /dev/null +++ b/app/components/modal/settings/account/username.jsx @@ -0,0 +1,77 @@ +// import dependencies +import PropTypes from 'prop-types'; + +// import local files +import Modal from '../../../ui/modal'; +import TextInput from '../../../ui/input'; +import { useState } from 'react'; +import handleChangeUsername from '../../../../handlers/settings/account/username'; +import { observer } from 'mobx-react-lite'; +import useContextStore from '../../../../context'; + +const SettingsAccountUsernameModal = ({ + title, + modalTitle, + id, + value, + closeModal, +}) => { + const [error, setError] = useState(null); + + const { + userStore: { updateUsingKey }, + } = useContextStore(); + + const handleError = (error) => { + setError(error); + }; + + const submit = async () => { + const value = document.getElementById(`settings-edit-${id}`).value; + const query = await handleChangeUsername( + value, + setError, + closeModal, + handleError + ); + + if (query === true) { + updateUsingKey('displayName', value); + } + }; + + return ( + <> + {modalTitle} + + + + + + + Cancel + + + Update + + + + ); +}; + +SettingsAccountUsernameModal.displayName = 'SettingsAccountUsernameModal'; + +SettingsAccountUsernameModal.propTypes = { + title: PropTypes.string.isRequired, + modalTitle: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + closeModal: PropTypes.func.isRequired, +}; + +export default observer(SettingsAccountUsernameModal); diff --git a/app/components/modal/settings/manage/approve.jsx b/app/components/modal/settings/manage/approve.jsx index 2822178..784fc4a 100644 --- a/app/components/modal/settings/manage/approve.jsx +++ b/app/components/modal/settings/manage/approve.jsx @@ -23,6 +23,7 @@ const MemberApproveModal = ({ member, onClose }) => { toast.success('User request approved successfully.'); onClose(); } catch (error) { + console.log(error); toast.error("Error approving user's request."); } }; diff --git a/app/components/modal/settings/manage/decline.jsx b/app/components/modal/settings/manage/decline.jsx index af71dbc..2c044df 100644 --- a/app/components/modal/settings/manage/decline.jsx +++ b/app/components/modal/settings/manage/decline.jsx @@ -22,6 +22,7 @@ const MemberDeclineModal = ({ member, onClose }) => { toast.success('User request declined successfully.'); onClose(); } catch (error) { + console.log(error); toast.error("Error declining user's request."); } }; diff --git a/app/components/modal/settings/manage/delete.jsx b/app/components/modal/settings/manage/delete.jsx index fa43b68..c48d035 100644 --- a/app/components/modal/settings/manage/delete.jsx +++ b/app/components/modal/settings/manage/delete.jsx @@ -22,6 +22,7 @@ const MemberDeleteModal = ({ member, onClose }) => { toast.success('User has been removed successfully.'); onClose(); } catch (error) { + console.log(error); toast.error("Error declining user's request."); } }; diff --git a/app/components/modal/settings/manage/permissions.jsx b/app/components/modal/settings/manage/permissions.jsx index f3db149..973111a 100644 --- a/app/components/modal/settings/manage/permissions.jsx +++ b/app/components/modal/settings/manage/permissions.jsx @@ -45,6 +45,7 @@ const MemberPermissionsModal = ({ member, onClose }) => { toast.success('User permissions updated successfully.'); onClose(); } catch (error) { + console.log(error); if (error.response?.status === 400) { return toast.error( "You don't have permission to assign this permission." diff --git a/app/components/register/verify.jsx b/app/components/register/verify.jsx index c081f0c..8eee8a4 100644 --- a/app/components/register/verify.jsx +++ b/app/components/register/verify.jsx @@ -19,6 +19,7 @@ const RegisterVerify = () => { return navigate('/'); } catch (error) { + console.log(error); toast.error('Something went wrong while verifying your account.'); } }; diff --git a/app/components/settings/about.jsx b/app/components/settings/about.jsx index 33125b7..e3e0307 100644 --- a/app/components/settings/about.jsx +++ b/app/components/settings/about.jsx @@ -1,7 +1,7 @@ import './about.scss'; const SetttingAbout = () => { - const version = import.meta.env.VITE_REACT_APP_VERSION || '0.3.11'; + const version = import.meta.env.VITE_REACT_APP_VERSION || '0.5.0'; return (
diff --git a/app/components/settings/account/avatar.jsx b/app/components/settings/account/avatar.jsx new file mode 100644 index 0000000..8a7ca85 --- /dev/null +++ b/app/components/settings/account/avatar.jsx @@ -0,0 +1,94 @@ +import './avatar.scss'; + +// import dependencies +import { observer } from 'mobx-react-lite'; + +// import local files +import useContextStore from '../../../context'; +import Button from '../../ui/button'; +import SettingsAccountAvatarModal from '../../modal/settings/account/avatar'; +import { toast } from 'sonner'; +import { createPostRequest } from '../../../services/axios'; + +const userPermissionNames = { 1: 'Owner', 2: 'Admin', 3: 'Editor', 4: 'Guest' }; + +const isImageUrl = (url) => { + if (typeof url !== 'string') { + return false; + } + return url.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif)$/gim) !== null; +}; + +const SettingsAccountAvatar = () => { + const { + userStore: { + user: { avatar, displayName, permission }, + updateUsingKey, + }, + modalStore: { openModal, closeModal }, + } = useContextStore(); + + const avatarUrl = isImageUrl(avatar) ? avatar : `/icons/${avatar}.png`; + + const userAvatar = avatar ? ( + + ) : ( +
+ {displayName?.charAt(0)} +
+ ); + + const handleAvatarChange = async (selectedAvatar) => { + try { + await createPostRequest('/api/user/update/avatar', { + avatar: selectedAvatar, + }); + + updateUsingKey('avatar', selectedAvatar); + + toast.success('Avatar successfully updated'); + closeModal(); + } catch (error) { + console.log(error); + toast.error('Something went wrong, please try again later.'); + } + }; + + return ( +
+ {userAvatar} +
+
+ {displayName} +
+
+ {userPermissionNames[permission]} +
+
+
+ + +
+
+ ); +}; + +export default observer(SettingsAccountAvatar); diff --git a/app/components/settings/account/avatar.scss b/app/components/settings/account/avatar.scss new file mode 100644 index 0000000..f41b2d5 --- /dev/null +++ b/app/components/settings/account/avatar.scss @@ -0,0 +1,61 @@ +@import '../../../styles/global.scss'; + +.settings-account-avatar-container { + display: flex; + align-items: center; + gap: pxToRem(15); + margin-bottom: pxToRem(20); +} + +.settings-account-avatar-image-default { + display: flex; + align-items: center; + justify-content: center; + width: pxToRem(150); + height: pxToRem(150); + border-radius: var(--radius-pill); + border: 3px solid var(--gray-600); + background-color: var(--primary-700); + font-size: var(--font-3xl); + color: var(--font-color); +} + +.settings-account-avatar-image { + display: flex; + width: pxToRem(150); + height: pxToRem(150); + border-radius: var(--radius-pill); + border: 3px solid var(--gray-600); +} + +.settings-account-avatar-button-container { + flex: 1; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 10px; +} + +@include tablet { + .settings-account-avatar-button-container { + flex-direction: column; + align-items: flex-end; + margin-top: 20px; + } +} + +@include mobile { + .settings-account-avatar-button-container { + flex-direction: row; + margin-top: 0; + } + + .settings-account-avatar-container { + flex-direction: column; + align-items: center; + } + + .settings-account-avatar-info { + text-align: center; + } +} diff --git a/app/components/settings/account/index.jsx b/app/components/settings/account/index.jsx new file mode 100644 index 0000000..0aadcd7 --- /dev/null +++ b/app/components/settings/account/index.jsx @@ -0,0 +1,70 @@ +import './../ui/tab/tab.scss'; +import './style.scss'; + +// import local files +import Button from '../../ui/button'; +import SettingsAccountDesktopItem from './item/desktop'; +import SettingsAccountMobileItem from './item/mobile'; +import SettingsAccountAvatar from './avatar'; + +const accountItems = [ + { + title: 'Email', + id: 'email', + }, + { + title: 'Username', + modalTitle: 'Edit your username', + id: 'displayName', + canEdit: true, + }, + { + title: 'Password', + modalTitle: 'Change Password', + id: 'password', + canEdit: true, + }, + { + title: 'Transfer Ownership', + id: 'transfer', + description: 'Transfer ownership to another user', + customButton: ( +
+ +
+ ), + }, + { + title: 'Delete Account', + id: 'delete', + description: 'Your account will be removed from our database', + customButton: ( +
+ +
+ ), + fontColor: 'red', + }, +]; + +const SettingsAccount = () => { + return ( +
+ + + + {accountItems.map((item) => ( + + ))} + + + + {accountItems.map((item) => ( + + ))} + +
+ ); +}; + +export default SettingsAccount; diff --git a/app/components/settings/account/item/desktop.jsx b/app/components/settings/account/item/desktop.jsx new file mode 100644 index 0000000..309853d --- /dev/null +++ b/app/components/settings/account/item/desktop.jsx @@ -0,0 +1,108 @@ +import './desktop.scss'; + +// import dependencies +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { observer } from 'mobx-react-lite'; + +// import local files +import Button from '../../../ui/button'; +import useContextStore from '../../../../context'; + +import SettingsAccountAvatarModal from '../../../modal/settings/account/avatar'; +import SettingsAccountDeleteModal from '../../../modal/settings/account/delete'; +import SettingsAccountPasswordModal from '../../../modal/settings/account/password'; +import SettingsAccountTransferModal from '../../../modal/settings/account/transfer'; +import SettingsAccountUsernameModal from '../../../modal/settings/account/username'; + +const selectModal = (id, props, closeModal) => { + switch (id) { + case 'displayName': + return ( + + ); + case 'password': + return ( + + ); + case 'avatar': + return ; + case 'transfer': + return ( + + ); + case 'delete': + return ; + default: + return null; + } +}; + +const SettingsAccountDesktopItem = ({ + title, + id, + canEdit, + description, + customButton, + ...props +}) => { + const classes = classNames({ + 'settings-account-item': !description, + 'settings-account-item-vertical': description, + }); + + const { + userStore: { user }, + modalStore: { openModal, closeModal }, + } = useContextStore(); + + return ( +
+
+
{title}
+ {!description && ( +
+ {id === 'password' ? '* * * * * * * *' : user[id]} +
+ )} + {description && ( +
{description}
+ )} +
+ { + const content = selectModal( + id, + { title, id, canEdit, value: user[id], ...props }, + closeModal + ); + + if (!content) { + return; + } + + openModal(content); + }} + > + {canEdit && !customButton && ( +
+ +
+ )} + {customButton ? customButton : null} +
+
+ ); +}; + +SettingsAccountDesktopItem.displayName = 'SettingsAccountDesktopItem'; + +SettingsAccountDesktopItem.propTypes = { + title: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + canEdit: PropTypes.bool, + description: PropTypes.string, + customButton: PropTypes.element, +}; + +export default observer(SettingsAccountDesktopItem); diff --git a/app/components/settings/account/item/desktop.scss b/app/components/settings/account/item/desktop.scss new file mode 100644 index 0000000..584d79f --- /dev/null +++ b/app/components/settings/account/item/desktop.scss @@ -0,0 +1,16 @@ +@import '../../../../styles/global.scss'; + +.settings-account-item-vertical { + display: flex; + flex-direction: column; + justify-content: flex-start; + margin-bottom: pxToRem(8); +} + +.settings-account-item-buttons { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 10px; + width: fit-content; +} diff --git a/app/components/settings/account/item/mobile.jsx b/app/components/settings/account/item/mobile.jsx new file mode 100644 index 0000000..d6537c5 --- /dev/null +++ b/app/components/settings/account/item/mobile.jsx @@ -0,0 +1,93 @@ +import './mobile.scss'; + +// import dependencies +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react-lite'; + +// import local files +import { FaChevronRight } from '../../../icons'; +import useContextStore from '../../../../context'; +import SettingsAccountAvatarModal from '../../../modal/settings/account/avatar'; +import SettingsAccountDeleteModal from '../../../modal/settings/account/delete'; +import SettingsAccountPasswordModal from '../../../modal/settings/account/password'; +import SettingsAccountTransferModal from '../../../modal/settings/account/transfer'; +import SettingsAccountEditModal from '../../../modal/settings/account/username'; + +const selectModal = (id, props, closeModal) => { + switch (id) { + case 'displayName': + return ; + case 'password': + return ( + + ); + case 'avatar': + return ; + case 'transfer': + return ( + + ); + case 'delete': + return ; + default: + return null; + } +}; + +const SettingsAccountMobileItem = ({ + title, + id, + canEdit, + fontColor, + ...props +}) => { + const { + userStore: { user }, + modalStore: { openModal, closeModal }, + } = useContextStore(); + + const color = !fontColor ? {} : { color: `var(--${fontColor}-700)` }; + + return ( +
{ + const content = selectModal( + id, + { title, id, canEdit, fontColor, value: user[id], ...props }, + closeModal + ); + + if (!content) { + return; + } + + openModal(content); + }} + > +
+ {title} +
+
+
{user[id]}
+ + {canEdit && ( +
+ +
+ )} +
+
+ ); +}; + +SettingsAccountMobileItem.displayName = 'SettingsAccountMobileItem'; + +SettingsAccountMobileItem.propTypes = { + title: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + canEdit: PropTypes.bool, + fontColor: PropTypes.string, +}; + +export default observer(SettingsAccountMobileItem); diff --git a/app/components/settings/account/item/mobile.scss b/app/components/settings/account/item/mobile.scss new file mode 100644 index 0000000..e68428a --- /dev/null +++ b/app/components/settings/account/item/mobile.scss @@ -0,0 +1,49 @@ +@import '../../../../styles/global.scss'; + +.settings-account-mobile-item { + display: flex; + padding: 12px 8px; + justify-content: center; + align-items: center; + border-bottom: 2px solid var(--accent-700); + user-select: none; + + &:hover { + cursor: pointer; + background-color: var(--accent-700); + border-radius: var(--radius-md); + } +} + +.settings-account-mobile-item-title { + font-size: var(--font-lg); + font-weight: var(--weight-semibold); + display: flex; + justify-content: center; + align-items: center; +} + +.settings-account-mobile-item:last-child { + border-bottom: none; +} + +.settings-account-mobile-item-description { + display: flex; + flex: 1; + justify-content: flex-end; + align-items: center; + gap: 8px; +} + +.settings-account-mobile-item-icon { + display: flex; + color: var(--accent-50); +} + +@include mobile { + .settings-account-mobile-item-icon { + width: 25px; + justify-content: center; + align-items: center; + } +} diff --git a/app/components/settings/account/style.scss b/app/components/settings/account/style.scss new file mode 100644 index 0000000..0866c0d --- /dev/null +++ b/app/components/settings/account/style.scss @@ -0,0 +1,64 @@ +@import '../../../styles/global.scss'; + +.settings-account-container { + padding: 0.75rem 1.5rem 0.75rem 1rem; + flex: 1; + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: pxToRem(1050); + margin-top: pxToRem(15); +} + +.settings-account-content { + display: flex; + flex: 1; + width: 100%; + max-width: pxToRem(800); + flex-direction: column; +} + +.settings-account-profile { + flex: 1; +} + +.settings-account-item { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: pxToRem(8); +} + +.settings-account-item-title { + font-weight: 600; + font-size: var(--font-md); + color: var(--accent-100); +} + +.settings-account-item-description { + font-size: var(--font-xl); +} + +.settings-account-item-button { + display: flex; + justify-content: center; + align-items: center; +} + +.settings-account-item-info { + font-size: var(--font-lg); +} + +@include mobile { + .settings-account-container { + padding: 0; + margin: 0 pxToRem(8); + } + + .settings-account-mobile-container { + flex: 1; + flex-direction: column; + background-color: var(--accent-800); + border-radius: var(--radius-md); + } +} diff --git a/app/components/settings/general/dropdown/colors.jsx b/app/components/settings/general/dropdown/colors.jsx deleted file mode 100644 index 818293d..0000000 --- a/app/components/settings/general/dropdown/colors.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import useDropdown from '../../../../hooks/useDropdown'; -import useLocalStorageContext from '../../../../hooks/useLocalstorage'; -import Dropdown from '../../../ui/dropdown/index'; - -const colors = ['Blue', 'Cyan', 'Green', 'Pink', 'Purple', 'Red', 'Yellow']; - -const ColorsDropdown = () => { - const { color: stateColor, setColor } = useLocalStorageContext(); - - const { dropdownIsOpen, toggleDropdown } = useDropdown(); - - const colorsList = colors.map((color) => ( - setColor(color)} - showDot - dotColor={color.toLowerCase()} - isSelected={stateColor === color} - > - {color} - - )); - - return ( - <> - - - - {stateColor} - - - {colorsList} - - - - ); -}; - -export default ColorsDropdown; diff --git a/app/components/settings/general/dropdown/dateFormat.jsx b/app/components/settings/general/dropdown/dateFormat.jsx deleted file mode 100644 index 7e64422..0000000 --- a/app/components/settings/general/dropdown/dateFormat.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import timeformats from '../../../../constant/dateformats.json'; -import useDropdown from '../../../../hooks/useDropdown'; -import useLocalStorageContext from '../../../../hooks/useLocalstorage'; -import Dropdown from '../../../ui/dropdown/index'; - -const DateFormatDropdown = () => { - const { dateformat, setDateformat } = useLocalStorageContext(); - - const { dropdownIsOpen, toggleDropdown } = useDropdown(); - - const dateFormatsList = timeformats.map((format) => ( - setDateformat(format)} - showDot - isSelected={dateformat === format} - > - {format} - - )); - - return ( - <> - - - - {dateformat} - - - {dateFormatsList} - - - - ); -}; - -export default DateFormatDropdown; diff --git a/app/components/settings/general/dropdown/index.jsx b/app/components/settings/general/dropdown/index.jsx deleted file mode 100644 index e9f1c6e..0000000 --- a/app/components/settings/general/dropdown/index.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import TimezoneDropdown from './timezone'; -import DateformatDropdown from './dateFormat'; -import TimeformatDropdown from './timeFormat'; -import ColorsDropdown from './colors'; -import ThemesDropdown from './themes'; - -export { - TimezoneDropdown, - DateformatDropdown, - TimeformatDropdown, - ColorsDropdown, - ThemesDropdown, -}; diff --git a/app/components/settings/general/dropdown/themes.jsx b/app/components/settings/general/dropdown/themes.jsx deleted file mode 100644 index dc612b3..0000000 --- a/app/components/settings/general/dropdown/themes.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import useDropdown from '../../../../hooks/useDropdown'; -import useLocalStorageContext from '../../../../hooks/useLocalstorage'; -import Dropdown from '../../../ui/dropdown/index'; - -const themes = { dark: 'Dark', light: 'Light' }; - -const ThemesDropdown = () => { - const { theme, setTheme } = useLocalStorageContext(); - const { dropdownIsOpen, toggleDropdown } = useDropdown(); - - return ( - <> - - - - {themes[theme]} - - - setTheme('dark')}>Dark - setTheme('light')}>Light - - - - ); -}; - -export default ThemesDropdown; diff --git a/app/components/settings/general/dropdown/timeFormat.jsx b/app/components/settings/general/dropdown/timeFormat.jsx deleted file mode 100644 index 564fe39..0000000 --- a/app/components/settings/general/dropdown/timeFormat.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import useDropdown from '../../../../hooks/useDropdown'; -import useLocalStorageContext from '../../../../hooks/useLocalstorage'; -import Dropdown from '../../../ui/dropdown/index'; - -const times = { - 'HH:mm:ss': '23:59:59', - 'HH:mm': '23:59', - 'hh:mm': '11:59', - 'hh:mm A': '11:59 PM', -}; - -const TimeFormatDropdown = () => { - const { timeformat, setTimeformat } = useLocalStorageContext(); - const { dropdownIsOpen, toggleDropdown } = useDropdown(); - - const timeFormatList = [ - { name: '23:59:59', value: 'HH:mm:ss' }, - { name: '23:59', value: 'HH:mm' }, - { name: '11:59', value: 'hh:mm' }, - { name: '11:59 PM', value: 'hh:mm A' }, - ].map((time) => ( - setTimeformat(time.value)} - showDot - isSelected={time.value === timeformat} - > - {time.name} - - )); - - return ( - <> - - - - {times[timeformat]} - - - {timeFormatList} - - - - ); -}; - -export default TimeFormatDropdown; diff --git a/app/components/settings/general/dropdown/timezone.jsx b/app/components/settings/general/dropdown/timezone.jsx deleted file mode 100644 index 23855c3..0000000 --- a/app/components/settings/general/dropdown/timezone.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import timezones from '../../../../constant/timeformats.json'; -import useDropdown from '../../../../hooks/useDropdown'; -import useLocalStorageContext from '../../../../hooks/useLocalstorage'; -import Dropdown from '../../../ui/dropdown/index'; - -const TimezoneDropdown = () => { - const { timezone, setTimezone } = useLocalStorageContext(); - const { dropdownIsOpen, toggleDropdown } = useDropdown(); - - const timezoneList = timezones.map((time) => ( - setTimezone(time.value)} - showDot - isSelected={time.value === timezone} - > - {time.name} - - )); - - return ( - <> - - - - {timezone} - - - {timezoneList} - - - - ); -}; - -export default TimezoneDropdown; diff --git a/app/components/settings/general/index.jsx b/app/components/settings/general/index.jsx deleted file mode 100644 index 966c8fc..0000000 --- a/app/components/settings/general/index.jsx +++ /dev/null @@ -1,81 +0,0 @@ -// import styles -import '../tab.scss'; -import './style.scss'; - -// import dependencies -import { observer } from 'mobx-react-lite'; - -// import local files -import TextInput from '../../ui/input'; -import { createPostRequest } from '../../../services/axios'; -import { - TimezoneDropdown, - DateformatDropdown, - TimeformatDropdown, - ColorsDropdown, - ThemesDropdown, -} from './dropdown'; -import AvatarSelect from '../../modal/settings/avatar'; -import useContextStore from '../../../context'; - -const SettingsGeneral = () => { - const { - userStore: { user, setUser }, - } = useContextStore(); - - const handleUpdate = async (e) => { - const value = e.target?.value?.trim(); - - if (!value) { - return; - } - - if (user.displayName !== value) { - await createPostRequest('/api/user/update/username', { - displayName: value, - }); - - setUser({ ...user, displayName: value }); - - return; - } - }; - - return ( -
-
General
- -
-
-
Profile Information
-
- Configure timezone and date format display settings -
- - - - - - - -
-
-
Date & Time
-
- Configure profile information and appearance -
- - - - -
-
-
- ); -}; - -export default observer(SettingsGeneral); diff --git a/app/components/settings/general/style.scss b/app/components/settings/general/style.scss deleted file mode 100644 index 9d73297..0000000 --- a/app/components/settings/general/style.scss +++ /dev/null @@ -1,62 +0,0 @@ -@import '../../../styles/global.scss'; - -.settings-general-container { - padding: pxToRem(4); - flex: 1; - display: flex; - flex-direction: column; -} - -.settings-general-content { - flex: 1; - display: flex; - gap: pxToRem(12); - padding: 0 pxToRem(8) pxToRem(20) pxToRem(8); - overflow-y: auto; -} - -.settings-general-profile { - flex: 1; -} - -.settings-general-date { - flex: 1; -} - -.settings-general-title { - color: var(--font-color); - font-size: var(--font-2xl); - font-weight: 700; -} - -.settings-general-subtitle { - color: var(--accent-200); - font-size: var(--font-lg); - font-weight: 500; - margin-bottom: pxToRem(12); -} - -.avatar-input { - width: 100%; - height: pxToRem(50); - background-color: var(--accent-800); - color: var(--font-color); - padding: 0 pxToRem(8); - font-size: var(--font-md); - font-weight: var(--weight-medium); - border-radius: var(--radius-md); - margin-top: pxToRem(4); - border: 2px solid #ffffff00; - display: flex; - align-items: center; - - &:hover { - cursor: pointer; - } -} - -@include tablet { - .settings-general-content { - flex-direction: column; - } -} diff --git a/app/components/settings/manage/index.jsx b/app/components/settings/manage/index.jsx index 416bb57..c605cf4 100644 --- a/app/components/settings/manage/index.jsx +++ b/app/components/settings/manage/index.jsx @@ -14,7 +14,9 @@ const ManageTeam = () => { const { getTeam, setTeam } = useTeamContext(); const team = getTeam(); - const sortedMembers = team?.sort((a, b) => a?.permission - b?.permission); + const sortedMembers = team + ?.sort((a, b) => a?.permission - b?.permission) + .sort((a, b) => a?.isVerified - b?.isVerified); useEffect(() => { const fetchTeam = async () => { @@ -23,6 +25,7 @@ const ManageTeam = () => { setTeam(query.data); } catch (error) { + console.log(error); toast.error("Couldn't fetch team members"); } }; @@ -33,15 +36,12 @@ const ManageTeam = () => { return (
-
- -
+
); }; diff --git a/app/components/settings/manage/member/row.jsx b/app/components/settings/manage/member/row.jsx index 96d614b..49bf2cf 100644 --- a/app/components/settings/manage/member/row.jsx +++ b/app/components/settings/manage/member/row.jsx @@ -10,11 +10,13 @@ import MemberRowActions from './actions'; import useContextStore from '../../../../context'; import { userPropType } from '../../../../utils/propTypes'; -const positions = { - 1: 'Owner', - 2: 'Admin', - 3: 'Editor', - 4: 'Guest', +const positions = { 1: 'Owner', 2: 'Admin', 3: 'Editor', 4: 'Guest' }; + +const isImageUrl = (url) => { + if (typeof url !== 'string') { + return false; + } + return url.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif)$/gim) !== null; }; const MemberTableRow = ({ member = {} }) => { @@ -38,12 +40,22 @@ const MemberTableRow = ({ member = {} }) => { const date = moment(member.createdAt).format('MMM DD, YYYY'); const time = moment(member.createdAt).format('hh:mm A'); - const avatar = member.avatar ? `/icons/${member.avatar}.png` : '/logo.svg'; + const avatarUrl = isImageUrl(member.avatar) + ? member.avatar + : `/icons/${member.avatar}.png`; + + const userAvatar = member.avatar ? ( + + ) : ( +
+ {member.displayName?.charAt(0)} +
+ ); return (
- + {userAvatar}
{member.displayName}
{member.email}
diff --git a/app/components/settings/manage/member/row.scss b/app/components/settings/manage/member/row.scss index 99e50c2..e3c815f 100644 --- a/app/components/settings/manage/member/row.scss +++ b/app/components/settings/manage/member/row.scss @@ -9,7 +9,20 @@ .member-row-image { width: pxToRem(45); height: pxToRem(45); - border-radius: 50%; + border-radius: var(--radius-pill); +} + +.member-row-image-default { + width: pxToRem(45); + height: pxToRem(45); + border-radius: var(--radius-pill); + display: flex; + align-items: center; + justify-content: center; + margin-right: pxToRem(6); + background-color: var(--primary-700); + font-size: var(--font-xl); + color: var(--font-color); } .member-row-details { diff --git a/app/components/settings/personalisation/accent.jsx b/app/components/settings/personalisation/accent.jsx new file mode 100644 index 0000000..fd11535 --- /dev/null +++ b/app/components/settings/personalisation/accent.jsx @@ -0,0 +1,56 @@ +// import dependencies +import PropTypes from 'prop-types'; + +// import local files +import FaCircleCheck from '../../icons/faCircleCheck'; +import classNames from 'classnames'; + +const colors = ['Blue', 'Purple', 'Green', 'Yellow', 'Red', 'Cyan', 'Pink']; + +const SettingsPersonalisationAccent = ({ color, setColor }) => { + return ( + <> +
Accent
+
+ {colors.map((colorName) => { + const classes = classNames( + 'settings-accent-container', + `settings-accent-${colorName.toLowerCase()}`, + { + 'settings-accent-blue--active': color === 'Blue', + 'settings-accent-cyan--active': color === 'Cyan', + 'settings-accent-green--active': color === 'Green', + 'settings-accent-pink--active': color === 'Pink', + 'settings-accent-purple--active': color === 'Purple', + 'settings-accent-red--active': color === 'Red', + 'settings-accent-yellow--active': color === 'Yellow', + } + ); + + return ( +
setColor(colorName)} + > + {color === colorName && ( +
+ +
+ )} +
+ ); + })} +
+ + ); +}; + +SettingsPersonalisationAccent.displayName = 'SettingsPersonalisationAccent'; + +SettingsPersonalisationAccent.propTypes = { + color: PropTypes.string.isRequired, + setColor: PropTypes.func.isRequired, +}; + +export default SettingsPersonalisationAccent; diff --git a/app/components/settings/personalisation/appearance.jsx b/app/components/settings/personalisation/appearance.jsx new file mode 100644 index 0000000..c13f89b --- /dev/null +++ b/app/components/settings/personalisation/appearance.jsx @@ -0,0 +1,80 @@ +// import dependencies +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react-lite'; +import dayjs from 'dayjs'; + +// import local files +import Button from '../../ui/button'; +import TextInput from '../../ui/input'; +import useContextStore from '../../../context'; + +const SettingsPersonalisationAppearance = ({ + dateformat, + timeformat, + theme, +}) => { + const { + userStore: { + user: { displayName }, + }, + } = useContextStore(); + + const message = + theme === 'dark' + ? 'Woow this looks so nice to my eyes' + : theme === 'light' + ? 'Why is it so bright, I can barely look at this' + : 'I have no clue, might be dark, might be light'; + + return ( + <> +
Appearance
+
+
+
+
+ {displayName?.charAt(0)?.toUpperCase()} +
+ +
+
+
+ {displayName} +
+
+ {dayjs(Date.now()).format(`${dateformat} ${timeformat}`)} +
+
+
Configure the appearance using the controls below
+
{message}
+
+
+
+ +
+
+ +
+ + +
+
+ + ); +}; + +SettingsPersonalisationAppearance.displayName = + 'SettingsPersonalisationAppearance'; + +SettingsPersonalisationAppearance.propTypes = { + dateformat: PropTypes.string, + timeformat: PropTypes.string, + theme: PropTypes.string, +}; + +export default observer(SettingsPersonalisationAppearance); diff --git a/app/components/settings/personalisation/dateformat.jsx b/app/components/settings/personalisation/dateformat.jsx new file mode 100644 index 0000000..ef63138 --- /dev/null +++ b/app/components/settings/personalisation/dateformat.jsx @@ -0,0 +1,53 @@ +// import dependencies +import PropTypes from 'prop-types'; + +// import local files +import Button from '../../ui/button'; +import Dropdown from '../../ui/dropdown'; +import useDropdown from '../../../hooks/useDropdown'; +import timeformats from '../../../constant/dateformats.json'; + +const SettingsPersonalisationDateformat = ({ dateformat, setDateformat }) => { + const { dropdownIsOpen, toggleDropdown } = useDropdown(); + + return ( +
+
Date Format
+ + + + + + {timeformats.map((format) => ( + setDateformat(format)} + showDot + isSelected={dateformat === format} + > + {format} + + ))} + + +
+ ); +}; + +SettingsPersonalisationDateformat.displayName = + 'SettingsPersonalisationDateformat'; + +SettingsPersonalisationDateformat.propTypes = { + dateformat: PropTypes.string.isRequired, + setDateformat: PropTypes.func.isRequired, +}; + +export default SettingsPersonalisationDateformat; diff --git a/app/components/settings/personalisation/index.jsx b/app/components/settings/personalisation/index.jsx new file mode 100644 index 0000000..fdff36c --- /dev/null +++ b/app/components/settings/personalisation/index.jsx @@ -0,0 +1,58 @@ +import './style.scss'; + +// import local files +import useLocalStorageContext from '../../../hooks/useLocalstorage'; +import SettingsPersonalisationAppearance from './appearance'; +import SettingsPersonalisationTheme from './theme'; +import SettingsPersonalisationAccent from './accent'; +import SettingsPersonalisationTimeformat from './timeformat'; +import SettingsPersonalisationDateformat from './dateformat'; +import SettingsPersonalisationTimezone from './timezone'; + +const SettingsPersonalisation = () => { + const { + timezone, + dateformat, + timeformat, + theme, + color, + setTimezone, + setDateformat, + setTimeformat, + setTheme, + setColor, + } = useLocalStorageContext(); + + return ( +
+
+ + + + +
+ + + +
+ + +
+
+ ); +}; + +export default SettingsPersonalisation; diff --git a/app/components/settings/personalisation/style.scss b/app/components/settings/personalisation/style.scss new file mode 100644 index 0000000..efb00cd --- /dev/null +++ b/app/components/settings/personalisation/style.scss @@ -0,0 +1,163 @@ +.settings-subtitle { + margin-top: 25px; + font-size: 20px; + font-weight: 700; + margin-bottom: 10px; +} + +.settings-appearance-container { + display: flex; + flex-direction: column; + background-color: var(--accent-900); + padding: 10px; + border-radius: 16px; +} + +.settings-appearance-icon { + width: 35px; + height: 35px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + background-color: var(--primary-700); + color: var(--font-color); + font-size: 18px; + font-weight: 700; +} + +.settings-appearance-info { + margin-bottom: 10px; +} + +.settings-appearance-displayname { + display: flex; + align-items: center; + gap: 5px; +} + +.settings-appearance-name { + font-weight: 700; + font-size: 18px; + margin-right: 3px; + + &:hover { + cursor: pointer; + color: var(--primary-500); + } +} + +.settings-appearance-time { + color: var(--accent-200); + font-size: 14px; +} + +.settings-appearance-content { + display: flex; + justify-content: space-between; + align-items: center; +} + +.settings-theme { + width: 4rem; + height: 4rem; + border-radius: 100%; + border: 2px solid var(--accent-300); + position: relative; + + &.settings-theme-light { + background-color: white; + } + + &.settings-theme-dark { + background-color: var(--gray-700); + } + + &.settings-theme--active { + border: 2px solid var(--primary-600); + } + + &:hover { + cursor: pointer; + } +} + +.settings-theme-icon { + position: absolute; + top: -3px; + right: -3px; + color: var(--primary-700); +} + +.settings-accent-container { + width: 3.5rem; + height: 3.5rem; + border-radius: 100%; + border: 2px solid var(--primary-700); + background: var(--primary-800); + cursor: pointer; + position: relative; + + &.settings-accent-blue { + border: 2px solid var(--blue-700); + background-color: var(--blue-800); + + &.settings-accent-blue--active { + border: 2px solid var(--blue-600); + } + } + + &.settings-accent-cyan { + border: 2px solid var(--cyan-700); + background-color: var(--cyan-800); + + &.settings-accent-cyan--active { + border: 2px solid var(--cyan-600); + } + } + + &.settings-accent-green { + border: 2px solid var(--green-700); + background-color: var(--green-800); + + &.settings-accent-green--active { + border: 2px solid var(--green-600); + } + } + + &.settings-accent-pink { + border: 2px solid var(--pink-700); + background-color: var(--pink-800); + + &.settings-accent-pink--active { + border: 2px solid var(--pink-600); + } + } + + &.settings-accent-purple { + border: 2px solid var(--purple-700); + background-color: var(--purple-800); + + &.settings-accent-purple--active { + border: 2px solid var(--purple-600); + } + } + + &.settings-accent-red { + border: 2px solid var(--red-700); + background-color: var(--red-800); + + &.settings-accent-red--active { + border: 2px solid var(--red-600); + } + } + + &.settings-accent-yellow { + border: 2px solid var(--yellow-700); + background-color: var(--yellow-800); + + &.settings-accent-yellow--active { + border: 2px solid var(--yellow-600); + } + } +} diff --git a/app/components/settings/personalisation/theme.jsx b/app/components/settings/personalisation/theme.jsx new file mode 100644 index 0000000..7863c43 --- /dev/null +++ b/app/components/settings/personalisation/theme.jsx @@ -0,0 +1,78 @@ +// import dependencies +import PropTypes from 'prop-types'; + +// import local files +import FaCircleCheck from '../../icons/faCircleCheck'; +import Tooltip from '../../ui/tooltip'; +import { LiaSyncSolid } from '../../icons'; + +const SettingsPersonalisationTheme = ({ theme, setTheme }) => { + return ( + <> +
Theme
+ +
+ +
setTheme('light')} + > + {theme === 'light' && ( +
+ +
+ )} +
+
+ +
setTheme('dark')} + > + {theme === 'dark' && ( +
+ +
+ )} +
+
+ +
setTheme('system')} + > + {theme === 'system' && ( +
+ +
+ )} +
+ +
+
+
+
+ + ); +}; + +SettingsPersonalisationTheme.displayName = 'SettingsPersonalisationTheme'; + +SettingsPersonalisationTheme.propTypes = { + theme: PropTypes.string.isRequired, + setTheme: PropTypes.func.isRequired, +}; + +export default SettingsPersonalisationTheme; diff --git a/app/components/settings/personalisation/timeformat.jsx b/app/components/settings/personalisation/timeformat.jsx new file mode 100644 index 0000000..aa15fd6 --- /dev/null +++ b/app/components/settings/personalisation/timeformat.jsx @@ -0,0 +1,59 @@ +// import dependencies +import PropTypes from 'prop-types'; + +// import local files +import Button from '../../ui/button'; +import Dropdown from '../../ui/dropdown'; +import useDropdown from '../../../hooks/useDropdown'; + +const times = { + 'HH:mm:ss': '23:59:59', + 'HH:mm': '23:59', + 'hh:mm': '11:59', + 'hh:mm A': '11:59 PM', +}; + +const SettingsPersonalisationTimeformat = ({ timeformat, setTimeformat }) => { + const { dropdownIsOpen, toggleDropdown } = useDropdown(); + + return ( +
+
Time Format
+ + + + + + {Object.keys(times).map((time) => ( + setTimeformat(time)} + showDot + isSelected={time === timeformat} + > + {times[time]} + + ))} + + +
+ ); +}; + +SettingsPersonalisationTimeformat.displayName = + 'SettingsPersonalisationTimeformat'; + +SettingsPersonalisationTimeformat.propTypes = { + timeformat: PropTypes.string.isRequired, + setTimeformat: PropTypes.func.isRequired, +}; + +export default SettingsPersonalisationTimeformat; diff --git a/app/components/settings/personalisation/timezone.jsx b/app/components/settings/personalisation/timezone.jsx new file mode 100644 index 0000000..337c1c8 --- /dev/null +++ b/app/components/settings/personalisation/timezone.jsx @@ -0,0 +1,47 @@ +// import dependencies +import PropTypes from 'prop-types'; + +// import local files +import Button from '../../ui/button'; +import Dropdown from '../../ui/dropdown'; +import timezonesList from '../../../constant/timezones.json'; +import useDropdown from '../../../hooks/useDropdown'; + +const SettingsPersonalisationTimezone = ({ timezone, setTimezone }) => { + const { dropdownIsOpen, toggleDropdown } = useDropdown(); + + return ( +
+
Timezone
+ + + + + + {Object.keys(timezonesList).map((timezone) => ( + setTimezone(timezone)}> + {timezonesList[timezone]} + + ))} + + +
+ ); +}; + +SettingsPersonalisationTimezone.displayName = 'SettingsPersonalisationTimezone'; + +SettingsPersonalisationTimezone.propTypes = { + timezone: PropTypes.string.isRequired, + setTimezone: PropTypes.func.isRequired, +}; + +export default SettingsPersonalisationTimezone; diff --git a/app/components/settings/tab.jsx b/app/components/settings/tab.jsx deleted file mode 100644 index 5527dea..0000000 --- a/app/components/settings/tab.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import './tab.scss'; - -// import dependencies -import PropTypes from 'prop-types'; - -const tabs = [ - { name: 'general', text: 'General' }, - { name: 'manage', text: 'Manage Team' }, - { name: 'about', text: 'About' }, -]; - -const SettingsTab = ({ tab, handleTabUpdate }) => { - const tabsList = tabs.map(({ name, text }) => { - const active = name === tab; - return ( -
handleTabUpdate(name)} - > - {text} -
- ); - }); - - return ( -
-
Settings
- {tabsList} -
- ); -}; - -SettingsTab.displayName = 'SettingsTab'; - -SettingsTab.propTypes = { - tab: PropTypes.string.isRequired, - handleTabUpdate: PropTypes.func.isRequired, -}; - -export default SettingsTab; diff --git a/app/components/settings/tab.scss b/app/components/settings/tab.scss deleted file mode 100644 index d93e6d9..0000000 --- a/app/components/settings/tab.scss +++ /dev/null @@ -1,39 +0,0 @@ -@import '../../styles/global.scss'; - -.settings-tab { - display: flex; - flex-direction: column; - width: 250px; - background-color: var(--accent-800); - padding: pxToRem(4); -} - -.settings-tab-title { - padding: pxToRem(12); - color: var(--accent-100); - font-size: var(--font-2xl); - font-weight: 700; - text-align: center; - border-bottom: pxToRem(3) solid var(--accent-600); - margin-bottom: pxToRem(12); -} - -.settings-tab-text { - color: var(--accent-100); - font-size: var(--font-2xl); - font-weight: 500; - cursor: pointer; - transition: background-color 0.2s ease-in-out; - margin: pxToRem(4); - padding: pxToRem(8) pxToRem(16); - border-radius: pxToRem(12); - - &:hover { - background-color: var(--accent-900); - } - - &.active { - background-color: var(--accent-900); - color: var(--primary-500); - } -} diff --git a/app/components/settings/ui/menu/desktop.jsx b/app/components/settings/ui/menu/desktop.jsx new file mode 100644 index 0000000..da1102a --- /dev/null +++ b/app/components/settings/ui/menu/desktop.jsx @@ -0,0 +1,35 @@ +// import dependencies +import PropTypes from 'prop-types'; + +// import local files +import SettingsTab from '../tab/desktop'; +import SettingsAccount from '../../account'; +import SettingsPersonalisation from '../../personalisation'; +import ManageTeam from '../../manage'; +import SettingsAbout from '../../about'; +import { FaClose } from '../../../icons'; + +const SettingsDesktop = ({ tab, handleTabUpdate, handleKeydown }) => { + return ( + <> +
handleKeydown(null, true)}> + +
+ + {tab === 'Account' && } + {tab === 'Appearance' && } + {tab === 'Manage Team' && } + {tab === 'About' && } + + ); +}; + +SettingsDesktop.displayName = 'SettingsDesktop'; + +SettingsDesktop.propTypes = { + tab: PropTypes.string.isRequired, + handleTabUpdate: PropTypes.func.isRequired, + handleKeydown: PropTypes.func.isRequired, +}; + +export default SettingsDesktop; diff --git a/app/components/settings/ui/menu/mobile.jsx b/app/components/settings/ui/menu/mobile.jsx new file mode 100644 index 0000000..732d5e8 --- /dev/null +++ b/app/components/settings/ui/menu/mobile.jsx @@ -0,0 +1,60 @@ +// import dependencies +import PropTypes from 'prop-types'; +import { useState } from 'react'; + +// import local files +import IoArrowBack from '../../../icons/ioArrowBack'; +import SettingsMobileTabs from '../tab/mobile'; +import SettingsAccount from '../../account'; +import SettingsPersonalisation from '../../personalisation'; +import ManageTeam from '../../manage'; +import SettingsAbout from '../../about'; + +const SettingsMobile = ({ handleKeydown }) => { + const [page, setPage] = useState('homepage'); + const handleTabChange = (page) => setPage(page); + + return ( +
+
+
{ + if (page === 'homepage') { + return handleKeydown(null, true); + } + + setPage('homepage'); + }} + > + +
+
+ Settings +
+
+ + {page === 'homepage' && ( + + )} + {page === 'Account' && } + {page === 'Appearance' && } + {page === 'Manage Team' && } + {page === 'About' && } +
+ ); +}; + +SettingsMobile.displayName = 'SettingsMobile'; + +SettingsMobile.propTypes = { + handleKeydown: PropTypes.func.isRequired, +}; + +export default SettingsMobile; diff --git a/app/components/settings/ui/tab/desktop.jsx b/app/components/settings/ui/tab/desktop.jsx new file mode 100644 index 0000000..e349932 --- /dev/null +++ b/app/components/settings/ui/tab/desktop.jsx @@ -0,0 +1,43 @@ +// import dependencies +import PropTypes from 'prop-types'; + +const tabs = [ + { title: 'GENERAL', items: ['Account', 'Appearance'] }, + { title: 'WORKSPACE', items: ['Manage Team', 'About'] }, +]; + +const SettingsTab = ({ tab, handleTabUpdate }, index) => { + const tabsList = tabs.map(({ title, items }) => { + const itemsList = items.map((name) => { + const active = name === tab; + return ( +
handleTabUpdate(name)} + id={name.replace(' ', '-')} + > + {name} +
+ ); + }); + + return ( +
+ {title &&
{title}
} +
{itemsList}
+
+ ); + }); + + return
{tabsList}
; +}; + +SettingsTab.displayName = 'SettingsTab'; + +SettingsTab.propTypes = { + tab: PropTypes.string.isRequired, + handleTabUpdate: PropTypes.func.isRequired, +}; + +export default SettingsTab; diff --git a/app/components/settings/ui/tab/mobile.jsx b/app/components/settings/ui/tab/mobile.jsx new file mode 100644 index 0000000..4a21215 --- /dev/null +++ b/app/components/settings/ui/tab/mobile.jsx @@ -0,0 +1,59 @@ +// import dependencies +import PropTypes from 'prop-types'; +import { + FaChevronRight, + FaUserCircle, + IoColorPalette, + MdHelpCircle, +} from '../../../icons'; +import FaUsers from '../../../icons/faUsers'; + +const tabs = [ + { + title: 'General Settings', + items: [ + { name: 'Account', icon: }, + { name: 'Appearance', icon: }, + ], + }, + { + title: 'Workspace Settings', + items: [ + { name: 'Manage Team', icon: }, + { name: 'About', icon: }, + ], + }, +]; + +const SettingsMobileTabs = ({ handleTabChange }) => { + const tabsList = tabs.map(({ title, items }) => { + const itemsList = items.map((item) => { + return ( +
handleTabChange(item.name)} + > + {item.icon} +
{item.name}
+ +
+ ); + }); + + return ( +
+ {title &&
{title}
} +
{itemsList}
+
+ ); + }); + + return
{tabsList}
; +}; + +SettingsMobileTabs.displayName = 'SettingsMobileTabs'; + +SettingsMobileTabs.propTypes = { handleTabChange: PropTypes.func.isRequired }; + +export default SettingsMobileTabs; diff --git a/app/components/settings/ui/tab/tab.scss b/app/components/settings/ui/tab/tab.scss new file mode 100644 index 0000000..72e6e21 --- /dev/null +++ b/app/components/settings/ui/tab/tab.scss @@ -0,0 +1,73 @@ +@import '../../../../styles/global.scss'; + +.settings-tab { + display: flex; + flex-direction: column; + width: 250px; + background-color: var(--accent-900); + padding: pxToRem(4); + border-right: 3px solid var(--accent-700); +} + +.settings-tab-title { + padding: pxToRem(12) 0 0 pxToRem(20); + color: var(--accent-200); + font-size: var(--font-sm); +} + +.settings-tab-text { + color: var(--accent-100); + font-size: var(--font-xl); + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease-in-out; + margin: pxToRem(4); + padding: pxToRem(8) pxToRem(16); + border-radius: pxToRem(12); + + &:hover { + background-color: var(--accent-900); + } + + &.active { + background-color: var(--accent-900); + color: var(--primary-500); + } +} + +.settings-mobile-tab-text { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + color: var(--font-color); + padding: 10px 0; + border-bottom: 2px solid var(--accent-800); + gap: 10px; + + &:last-child { + border-bottom: none; + } +} + +.settings-mobile-tab-title { + padding: 3px 8px; + color: var(--accent-200); + font-weight: var(--weight-semibold); +} + +.settings-mobile-tab-items { + display: flex; + flex-direction: column; + flex: 1; + margin: 8px; + padding: 10px; + background-color: var(--accent-900); + border-radius: var(--radius-md); +} + +.settings-mobile-tabs { + display: flex; + flex-direction: column; + flex: 1; +} diff --git a/app/components/ui/button.jsx b/app/components/ui/button.jsx index d466751..5089927 100644 --- a/app/components/ui/button.jsx +++ b/app/components/ui/button.jsx @@ -12,17 +12,20 @@ const Button = ({ iconLeft, iconRight, color, + outline, fullWidth, + tabIndex = 0, as: Wrapper = 'div', ...props }) => { const classes = classNames('button', { - [`button--${color}`]: color, + [`button--${color}`]: !outline && color, + [`button--${outline}-outline`]: !color && outline, 'button-fixed-width': !fullWidth, }); return ( - + {iconLeft} {children &&
{children}
} {iconRight} @@ -37,7 +40,9 @@ Button.propTypes = { iconLeft: PropTypes.node, iconRight: PropTypes.node, color: colorPropType, + outline: colorPropType, fullWidth: PropTypes.bool, + tabIndex: PropTypes.number, as: PropTypes.elementType, }; diff --git a/app/components/ui/button.scss b/app/components/ui/button.scss index c411f56..0138247 100644 --- a/app/components/ui/button.scss +++ b/app/components/ui/button.scss @@ -11,6 +11,7 @@ border: 2px solid var(--gray-600); gap: 10px; transition: all 0.25s ease-in-out; + width: 100%; } .button-fixed-width { @@ -117,3 +118,83 @@ border-color: var(--yellow-600); } } + +.button--blue-outline { + border-color: var(--blue-700); + + &:hover { + background-color: var(--blue-600); + border-color: var(--blue-600); + } +} + +.button--cyan-outline { + border-color: var(--cyan-700); + + &:hover { + background-color: var(--cyan-600); + border-color: var(--cyan-600); + } +} + +.button--gray-outline { + border-color: var(--gray-700); + + &:hover { + background-color: var(--gray-600); + border-color: var(--gray-600); + } +} + +.button--green-outline { + border-color: var(--green-700); + + &:hover { + background-color: var(--green-600); + border-color: var(--green-600); + } +} +.button--pink-outline { + border-color: var(--pink-700); + + &:hover { + background-color: var(--pink-600); + border-color: var(--pink-600); + } +} + +.button--primary-outline { + border-color: var(--primary-700); + + &:hover { + background-color: var(--primary-600); + border-color: var(--primary-600); + } +} + +.button--purple-outline { + border-color: var(--purple-700); + + &:hover { + background-color: var(--purple-600); + border-color: var(--purple-600); + } +} + +.button--red-outline { + border-color: var(--red-700); + + &:hover { + background-color: var(--red-600); + border-color: var(--red-600); + } +} + +.button--yellow-outline { + border-color: var(--yellow-700); + + &:hover { + background-color: var(--yellow-600); + border-color: var(--yellow-600); + } +} diff --git a/app/components/ui/dropdown/trigger.jsx b/app/components/ui/dropdown/trigger.jsx index 2891b3d..cb2dd3e 100644 --- a/app/components/ui/dropdown/trigger.jsx +++ b/app/components/ui/dropdown/trigger.jsx @@ -12,6 +12,7 @@ const Trigger = ({ showIcon, toggleDropdown, children, + tabIndex = 0, ...props }) => { const classes = classNames('dropdown-trigger', { @@ -23,7 +24,12 @@ const Trigger = ({ }); return ( -
+
{children} {showIcon && (
@@ -42,6 +48,7 @@ Trigger.propTypes = { icon: PropTypes.node, showIcon: PropTypes.bool, toggleDropdown: PropTypes.func.isRequired, + tabIndex: PropTypes.number, children: PropTypes.node.isRequired, }; diff --git a/app/components/ui/input.jsx b/app/components/ui/input.jsx index fed35f6..4ecfac6 100644 --- a/app/components/ui/input.jsx +++ b/app/components/ui/input.jsx @@ -1,10 +1,23 @@ // import styles +import classNames from 'classnames'; import './input.scss'; // import dependencies import PropTypes from 'prop-types'; -const TextInput = ({ label, id, error, ...props }) => { +const TextInput = ({ + label, + id, + error, + iconLeft, + iconRight, + tabIndex = 0, + ...props +}) => { + const classes = classNames('text-input', { + 'text-input-icon-left': iconLeft, + 'text-input-icon-right': iconRight, + }); return ( <> {label && ( @@ -12,7 +25,17 @@ const TextInput = ({ label, id, error, ...props }) => { {label} )} - +
+ {iconLeft &&
{iconLeft}
} + + {iconRight &&
{iconRight}
} +
{error && (