|
| 1 | +import React, { useContext } from 'react'; |
| 2 | +import { useDispatch, useSelector } from 'react-redux'; |
| 3 | +import { sortBy } from 'lodash'; |
| 4 | +import { Link } from 'react-router-dom'; |
| 5 | +import PropTypes from 'prop-types'; |
| 6 | + |
| 7 | +import { useTranslation } from 'react-i18next'; |
| 8 | +import NavDropdownMenu from '../../../../components/Nav/NavDropdownMenu'; |
| 9 | +import NavMenuItem from '../../../../components/Nav/NavMenuItem'; |
| 10 | +import { availableLanguages, languageKeyToLabel } from '../../../../i18n'; |
| 11 | +import getConfig from '../../../../utils/getConfig'; |
| 12 | +import { showToast } from '../../actions/toast'; |
| 13 | +import { setLanguage } from '../../actions/preferences'; |
| 14 | +import NavBar from '../../../../components/Nav/NavBar'; |
| 15 | +import CaretLeftIcon from '../../../../images/left-arrow.svg'; |
| 16 | +import LogoIcon from '../../../../images/p5js-logo-small.svg'; |
| 17 | +import { selectSketchPath } from '../../selectors/project'; |
| 18 | +import { metaKey, metaKeyName } from '../../../../utils/metaKey'; |
| 19 | +import { useSketchActions } from '../../hooks'; |
| 20 | +import { getIsUserOwner } from '../../selectors/users'; |
| 21 | +import { cloneProject } from '../../actions/project'; |
| 22 | +import { |
| 23 | + newFile, |
| 24 | + newFolder, |
| 25 | + showKeyboardShortcutModal, |
| 26 | + startSketch, |
| 27 | + stopSketch |
| 28 | +} from '../../actions/ide'; |
| 29 | +import { logoutUser } from '../../../User/actions'; |
| 30 | +import { CmControllerContext } from '../../pages/IDEView'; |
| 31 | + |
| 32 | +const Nav = ({ layout }) => ( |
| 33 | + <NavBar> |
| 34 | + <LeftLayout layout={layout} /> |
| 35 | + <UserMenu /> |
| 36 | + </NavBar> |
| 37 | +); |
| 38 | + |
| 39 | +Nav.propTypes = { |
| 40 | + layout: PropTypes.oneOf(['dashboard', 'project']) |
| 41 | +}; |
| 42 | + |
| 43 | +Nav.defaultProps = { |
| 44 | + layout: 'project' |
| 45 | +}; |
| 46 | + |
| 47 | +const LeftLayout = (props) => { |
| 48 | + switch (props.layout) { |
| 49 | + case 'dashboard': |
| 50 | + return <DashboardMenu />; |
| 51 | + case 'project': |
| 52 | + default: |
| 53 | + return <ProjectMenu />; |
| 54 | + } |
| 55 | +}; |
| 56 | + |
| 57 | +LeftLayout.propTypes = { |
| 58 | + layout: PropTypes.oneOf(['dashboard', 'project']) |
| 59 | +}; |
| 60 | + |
| 61 | +LeftLayout.defaultProps = { |
| 62 | + layout: 'project' |
| 63 | +}; |
| 64 | + |
| 65 | +const UserMenu = () => { |
| 66 | + const user = useSelector((state) => state.user); |
| 67 | + |
| 68 | + const isLoginEnabled = getConfig('LOGIN_ENABLED'); |
| 69 | + const isAuthenticated = user.authenticated; |
| 70 | + |
| 71 | + if (isLoginEnabled && isAuthenticated) { |
| 72 | + return <AuthenticatedUserMenu />; |
| 73 | + } else if (isLoginEnabled && !isAuthenticated) { |
| 74 | + return <UnauthenticatedUserMenu />; |
| 75 | + } |
| 76 | + |
| 77 | + return null; |
| 78 | +}; |
| 79 | + |
| 80 | +const DashboardMenu = () => { |
| 81 | + const { t } = useTranslation(); |
| 82 | + const editorLink = useSelector(selectSketchPath); |
| 83 | + return ( |
| 84 | + <ul className="nav__items-left"> |
| 85 | + <li className="nav__item-logo"> |
| 86 | + <LogoIcon |
| 87 | + role="img" |
| 88 | + aria-label={t('Common.p5logoARIA')} |
| 89 | + focusable="false" |
| 90 | + className="svg__logo" |
| 91 | + /> |
| 92 | + </li> |
| 93 | + <li className="nav__item nav__item--no-icon"> |
| 94 | + <Link to={editorLink} className="nav__back-link"> |
| 95 | + <CaretLeftIcon |
| 96 | + className="nav__back-icon" |
| 97 | + focusable="false" |
| 98 | + aria-hidden="true" |
| 99 | + /> |
| 100 | + <span className="nav__item-header">{t('Nav.BackEditor')}</span> |
| 101 | + </Link> |
| 102 | + </li> |
| 103 | + </ul> |
| 104 | + ); |
| 105 | +}; |
| 106 | + |
| 107 | +const ProjectMenu = (props) => { |
| 108 | + const isUserOwner = useSelector(getIsUserOwner); |
| 109 | + const project = useSelector((state) => state?.project); |
| 110 | + const user = useSelector((state) => state.user); |
| 111 | + |
| 112 | + // TODO: use selectRootFile selector |
| 113 | + const rootFile = useSelector( |
| 114 | + (state) => state.files.filter((file) => file.name === 'root')[0] |
| 115 | + ); |
| 116 | + |
| 117 | + const cmRef = useContext(CmControllerContext); |
| 118 | + |
| 119 | + const dispatch = useDispatch(); |
| 120 | + |
| 121 | + const { t } = useTranslation(); |
| 122 | + const { |
| 123 | + newSketch, |
| 124 | + saveSketch, |
| 125 | + downloadSketch, |
| 126 | + shareSketch |
| 127 | + } = useSketchActions(); |
| 128 | + |
| 129 | + const replaceCommand = |
| 130 | + metaKey === 'Ctrl' ? `${metaKeyName}+H` : `${metaKeyName}+⌥+F`; |
| 131 | + |
| 132 | + return ( |
| 133 | + <ul className="nav__items-left"> |
| 134 | + <li className="nav__item-logo"> |
| 135 | + <LogoIcon |
| 136 | + role="img" |
| 137 | + aria-label={t('Common.p5logoARIA')} |
| 138 | + focusable="false" |
| 139 | + className="svg__logo" |
| 140 | + /> |
| 141 | + </li> |
| 142 | + <NavDropdownMenu id="file" title={t('Nav.File.Title')}> |
| 143 | + <NavMenuItem onClick={newSketch}>{t('Nav.File.New')}</NavMenuItem> |
| 144 | + <NavMenuItem |
| 145 | + hideIf={ |
| 146 | + !getConfig('LOGIN_ENABLED') || (project?.owner && !isUserOwner) |
| 147 | + } |
| 148 | + onClick={() => saveSketch(cmRef)} |
| 149 | + > |
| 150 | + {t('Common.Save')} |
| 151 | + <span className="nav__keyboard-shortcut">{metaKeyName}+S</span> |
| 152 | + </NavMenuItem> |
| 153 | + <NavMenuItem |
| 154 | + hideIf={!project?.id || !user.authenticated} |
| 155 | + onClick={() => dispatch(cloneProject())} |
| 156 | + > |
| 157 | + {t('Nav.File.Duplicate')} |
| 158 | + </NavMenuItem> |
| 159 | + <NavMenuItem hideIf={!project?.id} onClick={shareSketch}> |
| 160 | + {t('Nav.File.Share')} |
| 161 | + </NavMenuItem> |
| 162 | + <NavMenuItem hideIf={!project?.id} onClick={downloadSketch}> |
| 163 | + {t('Nav.File.Download')} |
| 164 | + </NavMenuItem> |
| 165 | + <NavMenuItem |
| 166 | + hideIf={!user.authenticated} |
| 167 | + href={`/${user.username}/sketches`} |
| 168 | + > |
| 169 | + {t('Nav.File.Open')} |
| 170 | + </NavMenuItem> |
| 171 | + <NavMenuItem |
| 172 | + hideIf={ |
| 173 | + !getConfig('UI_COLLECTIONS_ENABLED') || |
| 174 | + !user.authenticated || |
| 175 | + !project?.id |
| 176 | + } |
| 177 | + href={`/${user.username}/sketches/${project?.id}/add-to-collection`} |
| 178 | + > |
| 179 | + {t('Nav.File.AddToCollection')} |
| 180 | + </NavMenuItem> |
| 181 | + <NavMenuItem |
| 182 | + hideIf={!getConfig('EXAMPLES_ENABLED')} |
| 183 | + href="/p5/sketches" |
| 184 | + > |
| 185 | + {t('Nav.File.Examples')} |
| 186 | + </NavMenuItem> |
| 187 | + </NavDropdownMenu> |
| 188 | + <NavDropdownMenu id="edit" title={t('Nav.Edit.Title')}> |
| 189 | + <NavMenuItem onClick={cmRef.current?.tidyCode}> |
| 190 | + {t('Nav.Edit.TidyCode')} |
| 191 | + <span className="nav__keyboard-shortcut"> |
| 192 | + {metaKeyName}+{'\u21E7'}+F |
| 193 | + </span> |
| 194 | + </NavMenuItem> |
| 195 | + <NavMenuItem onClick={cmRef.current?.showFind}> |
| 196 | + {t('Nav.Edit.Find')} |
| 197 | + <span className="nav__keyboard-shortcut">{metaKeyName}+F</span> |
| 198 | + </NavMenuItem> |
| 199 | + <NavMenuItem onClick={cmRef.current?.showReplace}> |
| 200 | + {t('Nav.Edit.Replace')} |
| 201 | + <span className="nav__keyboard-shortcut">{replaceCommand}</span> |
| 202 | + </NavMenuItem> |
| 203 | + </NavDropdownMenu> |
| 204 | + <NavDropdownMenu id="sketch" title={t('Nav.Sketch.Title')}> |
| 205 | + <NavMenuItem onClick={() => dispatch(newFile(rootFile.id))}> |
| 206 | + {t('Nav.Sketch.AddFile')} |
| 207 | + </NavMenuItem> |
| 208 | + <NavMenuItem onClick={() => dispatch(newFolder(rootFile.id))}> |
| 209 | + {t('Nav.Sketch.AddFolder')} |
| 210 | + </NavMenuItem> |
| 211 | + <NavMenuItem onClick={() => dispatch(startSketch())}> |
| 212 | + {t('Nav.Sketch.Run')} |
| 213 | + <span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span> |
| 214 | + </NavMenuItem> |
| 215 | + <NavMenuItem onClick={() => dispatch(stopSketch())}> |
| 216 | + {t('Nav.Sketch.Stop')} |
| 217 | + <span className="nav__keyboard-shortcut"> |
| 218 | + {'\u21E7'}+{metaKeyName}+Enter |
| 219 | + </span> |
| 220 | + </NavMenuItem> |
| 221 | + </NavDropdownMenu> |
| 222 | + <NavDropdownMenu id="help" title={t('Nav.Help.Title')}> |
| 223 | + <NavMenuItem onClick={() => dispatch(showKeyboardShortcutModal())}> |
| 224 | + {t('Nav.Help.KeyboardShortcuts')} |
| 225 | + </NavMenuItem> |
| 226 | + <NavMenuItem href="https://p5js.org/reference/"> |
| 227 | + {t('Nav.Help.Reference')} |
| 228 | + </NavMenuItem> |
| 229 | + <NavMenuItem href="/about">{t('Nav.Help.About')}</NavMenuItem> |
| 230 | + </NavDropdownMenu> |
| 231 | + </ul> |
| 232 | + ); |
| 233 | +}; |
| 234 | + |
| 235 | +const LanguageMenu = () => { |
| 236 | + const language = useSelector((state) => state.preferences.language); |
| 237 | + const dispatch = useDispatch(); |
| 238 | + |
| 239 | + function handleLangSelection(event) { |
| 240 | + dispatch(setLanguage(event.target.value)); |
| 241 | + dispatch(showToast('Toast.LangChange')); |
| 242 | + } |
| 243 | + |
| 244 | + return ( |
| 245 | + <NavDropdownMenu id="lang" title={languageKeyToLabel(language)}> |
| 246 | + {sortBy(availableLanguages).map((key) => ( |
| 247 | + // eslint-disable-next-line react/jsx-no-bind |
| 248 | + <NavMenuItem key={key} value={key} onClick={handleLangSelection}> |
| 249 | + {languageKeyToLabel(key)} |
| 250 | + </NavMenuItem> |
| 251 | + ))} |
| 252 | + </NavDropdownMenu> |
| 253 | + ); |
| 254 | +}; |
| 255 | + |
| 256 | +const UnauthenticatedUserMenu = () => { |
| 257 | + const { t } = useTranslation(); |
| 258 | + return ( |
| 259 | + <ul className="nav__items-right" title="user-menu"> |
| 260 | + {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />} |
| 261 | + <li className="nav__item"> |
| 262 | + <Link to="/login" className="nav__auth-button"> |
| 263 | + <span className="nav__item-header" title="Login"> |
| 264 | + {t('Nav.Login')} |
| 265 | + </span> |
| 266 | + </Link> |
| 267 | + </li> |
| 268 | + <span className="nav__item-or">{t('Nav.LoginOr')}</span> |
| 269 | + <li className="nav__item"> |
| 270 | + <Link to="/signup" className="nav__auth-button"> |
| 271 | + <span className="nav__item-header" title="SignUp"> |
| 272 | + {t('Nav.SignUp')} |
| 273 | + </span> |
| 274 | + </Link> |
| 275 | + </li> |
| 276 | + </ul> |
| 277 | + ); |
| 278 | +}; |
| 279 | + |
| 280 | +const AuthenticatedUserMenu = () => { |
| 281 | + const user = useSelector((state) => state.user); |
| 282 | + |
| 283 | + const { t } = useTranslation(); |
| 284 | + const dispatch = useDispatch(); |
| 285 | + |
| 286 | + return ( |
| 287 | + <ul className="nav__items-right" title="user-menu"> |
| 288 | + {getConfig('TRANSLATIONS_ENABLED') && <LanguageMenu />} |
| 289 | + <NavDropdownMenu |
| 290 | + id="account" |
| 291 | + title={ |
| 292 | + <span> |
| 293 | + {t('Nav.Auth.Hello')}, {user.username}! |
| 294 | + </span> |
| 295 | + } |
| 296 | + > |
| 297 | + <NavMenuItem href={`/${user.username}/sketches`}> |
| 298 | + {t('Nav.Auth.MySketches')} |
| 299 | + </NavMenuItem> |
| 300 | + <NavMenuItem |
| 301 | + href={`/${user.username}/collections`} |
| 302 | + hideIf={!getConfig('UI_COLLECTIONS_ENABLED')} |
| 303 | + > |
| 304 | + {t('Nav.Auth.MyCollections')} |
| 305 | + </NavMenuItem> |
| 306 | + <NavMenuItem href={`/${user.username}/assets`}> |
| 307 | + {t('Nav.Auth.MyAssets')} |
| 308 | + </NavMenuItem> |
| 309 | + <NavMenuItem href="/account">{t('Preferences.Settings')}</NavMenuItem> |
| 310 | + <NavMenuItem onClick={() => dispatch(logoutUser())}> |
| 311 | + {t('Nav.Auth.LogOut')} |
| 312 | + </NavMenuItem> |
| 313 | + </NavDropdownMenu> |
| 314 | + </ul> |
| 315 | + ); |
| 316 | +}; |
| 317 | + |
| 318 | +export default Nav; |
0 commit comments