diff --git a/src/actions/app.ts b/src/actions/app.ts index 7864f2e5..e47531a1 100644 --- a/src/actions/app.ts +++ b/src/actions/app.ts @@ -13,6 +13,10 @@ export enum AppActionType { OpenSettings = 'app.action.openSettings', /** Close settings dialog. */ CloseSettings = 'app.action.closeSettings', + /** Open about dialog. */ + OpenAboutDialog = 'app.action.openAboutDialog', + /** Close about dialog. */ + CloseAboutDialog = 'app.action.closeAboutDialog', } /** Action that indicates the app has just started. */ @@ -39,8 +43,26 @@ export function closeSettings(): AppCloseSettingsAction { return { type: AppActionType.CloseSettings }; } +/** Action to open the about dialog. */ +export type AppOpenAboutDialogAction = Action; + +/** Creates an action to open the about dialog. */ +export function openAboutDialog(): AppOpenAboutDialogAction { + return { type: AppActionType.OpenAboutDialog }; +} + +/** Action to close the about dialog. */ +export type AppCloseAboutDialogAction = Action; + +/** Creates an action to close the about dialog. */ +export function closeAboutDialog(): AppCloseAboutDialogAction { + return { type: AppActionType.CloseAboutDialog }; +} + /** common type for all app actions. */ export type AppAction = | AppStartupAction | AppOpenSettingsAction - | AppCloseSettingsAction; + | AppCloseSettingsAction + | AppOpenAboutDialogAction + | AppCloseAboutDialogAction; diff --git a/src/components/AboutDialog.tsx b/src/components/AboutDialog.tsx new file mode 100644 index 00000000..f219bb3e --- /dev/null +++ b/src/components/AboutDialog.tsx @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +// The about dialog + +import { AnchorButton, Classes, Dialog } from '@blueprintjs/core'; +import { WithI18nProps, withI18n } from '@shopify/react-i18n'; +import React from 'react'; +import { connect } from 'react-redux'; +import { Action, Dispatch } from '../actions'; +import { closeAboutDialog } from '../actions/app'; +import { RootState } from '../reducers'; +import { + appName, + legoDisclaimer, + pybricksCopyright, + pybricksWebsiteUrl, +} from '../settings/ui'; +import { AboutStringId } from './about-i18n'; +import en from './about-i18n.en.json'; + +type StateProps = { showAboutDialog: boolean }; + +type DispatchProps = { onClose: () => void }; + +type AboutDialogProps = StateProps & DispatchProps & WithI18nProps; + +class AboutDialog extends React.Component { + render(): JSX.Element { + const { i18n, showAboutDialog, onClose } = this.props; + return ( + onClose()}> +
+

+ {i18n.translate(AboutStringId.Description)} +

+

{pybricksCopyright}

+
+
+

{legoDisclaimer}

+
+ + {i18n.translate(AboutStringId.WebsiteButtonLabel)} + +
+
+
+ ); + } +} + +const mapStateToProps = (state: RootState): StateProps => ({ + showAboutDialog: state.app.showAboutDialog, +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + onClose: (): Action => dispatch(closeAboutDialog()), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withI18n({ id: 'about', fallback: en, translations: { en } })(AboutDialog)); diff --git a/src/components/App.tsx b/src/components/App.tsx index 04a15162..f42eb9c9 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -5,6 +5,7 @@ import React, { useState } from 'react'; import { useSelector } from 'react-redux'; import SplitterLayout from 'react-splitter-layout'; import { RootState } from '../reducers'; +import AboutDialog from './AboutDialog'; import Editor from './Editor'; import SettingsDrawer from './SettingsDrawer'; import StatusBar from './StatusBar'; @@ -64,6 +65,7 @@ function App(): JSX.Element { + ); } diff --git a/src/components/SettingsDrawer.tsx b/src/components/SettingsDrawer.tsx index 6cd44b60..056c65ea 100644 --- a/src/components/SettingsDrawer.tsx +++ b/src/components/SettingsDrawer.tsx @@ -6,7 +6,7 @@ import { WithI18nProps, withI18n } from '@shopify/react-i18n'; import React from 'react'; import { connect } from 'react-redux'; import { Action, Dispatch } from '../actions'; -import { closeSettings } from '../actions/app'; +import { closeSettings, openAboutDialog } from '../actions/app'; import { setBoolean } from '../actions/settings'; import { RootState } from '../reducers'; import { tooltipDelay } from '../settings/ui'; @@ -29,6 +29,7 @@ type DispatchProps = { onShowDocsChanged: (checked: boolean) => void; onDarkModeChanged: (checked: boolean) => void; onFlashCurrentProgramChanged: (checked: boolean) => void; + onAbout: () => void; }; type SettingsProps = StateProps & DispatchProps & WithI18nProps; @@ -45,6 +46,7 @@ class SettingsDrawer extends React.PureComponent { onDarkModeChanged, flashCurrentProgram, onFlashCurrentProgramChanged, + onAbout, } = this.props; return ( { /> + + { + onAbout(); + return true; + }} + > + {i18n.translate(SettingsStringId.HelpAboutLabel)} + + ); @@ -152,6 +164,7 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ dispatch(setBoolean(SettingId.DarkMode, checked)), onFlashCurrentProgramChanged: (checked): Action => dispatch(setBoolean(SettingId.FlashCurrentProgram, checked)), + onAbout: (): Action => dispatch(openAboutDialog()), }); export default connect( diff --git a/src/components/about-i18n.en.json b/src/components/about-i18n.en.json new file mode 100644 index 00000000..820e65fb --- /dev/null +++ b/src/components/about-i18n.en.json @@ -0,0 +1,6 @@ +{ + "about": { + "description": "A simple web app for programming LEGO® Powered Up smart hubs using Pybricks MicroPython.", + "websiteButton": { "label": "Pybricks Website" } + } +} diff --git a/src/components/about-i18n.test.ts b/src/components/about-i18n.test.ts new file mode 100644 index 00000000..3c94a2b1 --- /dev/null +++ b/src/components/about-i18n.test.ts @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { lookup } from '../../test'; +import { AboutStringId } from './about-i18n'; +import en from './about-i18n.en.json'; + +describe('Ensure .json file has matches for AboutStringId', () => { + test.each(Object.values(AboutStringId))('%s', (id) => { + expect(lookup(en, id)).toBeDefined(); + }); +}); diff --git a/src/components/about-i18n.ts b/src/components/about-i18n.ts new file mode 100644 index 00000000..b73a4ef4 --- /dev/null +++ b/src/components/about-i18n.ts @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +// About dialog translation keys. + +export enum AboutStringId { + Description = 'about.description', + WebsiteButtonLabel = 'about.websiteButton.label', +} diff --git a/src/components/settings-i18n.en.json b/src/components/settings-i18n.en.json index 72baa0e7..f13be9a3 100644 --- a/src/components/settings-i18n.en.json +++ b/src/components/settings-i18n.en.json @@ -1,6 +1,6 @@ { "settings": { - "title": "Settings", + "title": "Settings & Help", "appearance": { "title": "Appearance", "documentation": { @@ -21,6 +21,12 @@ "label": "Include current program", "tooltip": "Select to include your program when installing the firmware." } + }, + "help": { + "title": "Help", + "about": { + "label": "About" + } } } } diff --git a/src/components/settings-i18n.ts b/src/components/settings-i18n.ts index 6dc5c3fd..48a11306 100644 --- a/src/components/settings-i18n.ts +++ b/src/components/settings-i18n.ts @@ -14,4 +14,6 @@ export enum SettingsStringId { FirmwareTitle = 'settings.firmware.title', FirmwareCurrentProgramLabel = 'settings.firmware.flash-current-program.label', FirmwareCurrentProgramTooltip = 'settings.firmware.flash-current-program.tooltip', + HelpTitle = 'settings.help.title', + HelpAboutLabel = 'settings.help.about.label', } diff --git a/src/components/settings.scss b/src/components/settings.scss index a1c23ce4..18fcfb4a 100644 --- a/src/components/settings.scss +++ b/src/components/settings.scss @@ -15,3 +15,7 @@ .pb-settings .bp3-form-helper-text { font-size: 14px; } + +.pb-settings a { + font-size: 16px; +} diff --git a/src/reducers/app.ts b/src/reducers/app.ts index ca68133a..5339d975 100644 --- a/src/reducers/app.ts +++ b/src/reducers/app.ts @@ -7,6 +7,11 @@ import { Reducer, combineReducers } from 'redux'; import { Action } from '../actions'; import { AppActionType } from '../actions/app'; +export interface AppState { + readonly showSettings: boolean; + readonly showAboutDialog: boolean; +} + const showSettings: Reducer = (state = false, action) => { switch (action.type) { case AppActionType.OpenSettings: @@ -18,8 +23,15 @@ const showSettings: Reducer = (state = false, action) => { } }; -export interface AppState { - readonly showSettings: boolean; -} +const showAboutDialog: Reducer = (state = false, action) => { + switch (action.type) { + case AppActionType.OpenAboutDialog: + return true; + case AppActionType.CloseAboutDialog: + return false; + default: + return state; + } +}; -export default combineReducers({ showSettings }); +export default combineReducers({ showSettings, showAboutDialog }); diff --git a/src/settings/ui.ts b/src/settings/ui.ts index b9916a6c..9bb44b99 100644 --- a/src/settings/ui.ts +++ b/src/settings/ui.ts @@ -5,3 +5,17 @@ /** Time in milliseconds before showing tooltip popover. */ export const tooltipDelay = 1000; + +/** Official name of the app. */ +export const appName = 'Pybricks Code'; + +/** URL to main Pybricks website. */ +export const pybricksWebsiteUrl = 'https://pybricks.com'; + +/** Pybricks copyright statement. */ +export const pybricksCopyright = 'Copyright (c) 2020-2021 The Pybricks Authors'; + +// https://www.lego.com/en-us/legal/notices-and-policies/fair-play/ +/** LEGO "fair play" disclaimer */ +export const legoDisclaimer = + 'LEGO® is a trademark of the LEGO Group of companies which does not sponsor, authorize or endorse this site.';