Skip to content

Commit ca8296a

Browse files
committed
context and the new nav
1 parent 279364f commit ca8296a

File tree

6 files changed

+438
-5
lines changed

6 files changed

+438
-5
lines changed
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
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;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { useSelector } from 'react-redux';
4+
import MediaQuery from 'react-responsive';
5+
import Toolbar from '../Toolbar';
6+
import Nav from './Nav';
7+
8+
const Header = (props) => {
9+
const project = useSelector((state) => state.project);
10+
11+
return (
12+
<header style={{ zIndex: 10 }}>
13+
<Nav />
14+
<MediaQuery minWidth={770}>
15+
{(matches) => {
16+
if (matches)
17+
return (
18+
<Toolbar
19+
syncFileContent={props.syncFileContent}
20+
key={project.id}
21+
/>
22+
);
23+
return null;
24+
}}
25+
</MediaQuery>
26+
</header>
27+
);
28+
};
29+
30+
Header.propTypes = {
31+
syncFileContent: PropTypes.func.isRequired
32+
};
33+
34+
export default Header;

0 commit comments

Comments
 (0)