diff --git a/index.html b/index.html index ede552fbb..4d21ebbaf 100644 --- a/index.html +++ b/index.html @@ -16,11 +16,11 @@ diff --git a/lang/ui.en.json b/lang/ui.en.json index ddf02b5a2..3cbd0be48 100644 --- a/lang/ui.en.json +++ b/lang/ui.en.json @@ -679,13 +679,9 @@ "defaultMessage": "Live graph", "description": "" }, - "footer.resume": { - "defaultMessage": "Resume session", - "description": "" - }, - "footer.start": { - "defaultMessage": "Start new session", - "description": "" + "get-started-action": { + "defaultMessage": "Get started", + "description": "Get started action" }, "help-label": { "defaultMessage": "Help", @@ -699,9 +695,33 @@ "defaultMessage": "Home", "description": "Home button text" }, + "homepage-description": { + "defaultMessage": "Train a machine learning model on your own movement data and run it on your micro:bit.", + "description": "Home page description" + }, + "homepage-how-it-works": { + "defaultMessage": "How it works", + "description": "Home page section heading" + }, + "homepage-projects": { + "defaultMessage": "Projects", + "description": "Home page section heading" + }, + "homepage-step-by-step": { + "defaultMessage": "Step by step", + "description": "Home page section heading" + }, "homepage-subtitle": { - "defaultMessage": "Introduce students to machine learning concepts through physical movement and data", - "description": "Subtitle of the home page" + "defaultMessage": "Create AI on your BBC micro:bit using movement and machine learning.", + "description": "Home page subtitle" + }, + "homepage-video-alt": { + "defaultMessage": "Introductory video", + "description": "Home page video alt text" + }, + "homepage-video-prompt": { + "defaultMessage": "Watch the video to learn how to use {appNameFull}.", + "description": "Prompt to watch the video on the home page" }, "homepage.Link": { "defaultMessage": "Home page", @@ -743,6 +763,82 @@ "defaultMessage": "More edit in MakeCode options", "description": "Aria label for the additional actions menu to the right of the Edit in MakeCode button" }, + "newpage-browse-lessons": { + "defaultMessage": "Browse lessons", + "description": "Browse lessons button text" + }, + "newpage-browse-projects": { + "defaultMessage": "Browse projects", + "description": "Browse projects button text" + }, + "newpage-choose-subtitle": { + "defaultMessage": "Find a project or lesson from our teacher resources and open it in micro:bit classroom", + "description": "Choose a project or lesson subtitle" + }, + "newpage-choose-title": { + "defaultMessage": "Choose a project or lesson", + "description": "Choose a project or lesson title" + }, + "newpage-continue-session-instruction": { + "defaultMessage": "To continue a saved session from a file please select a file.", + "description": "Instruction to users to continue a saved session from a file" + }, + "newpage-continue-session-subtitle": { + "defaultMessage": "Use a hex file or data samples file you saved to your computer to continue a session.", + "description": "Continue session subtitle" + }, + "newpage-continue-session-title": { + "defaultMessage": "Continue a saved session", + "description": "Continue session title" + }, + "newpage-heading-subtitle": { + "defaultMessage": "Run whole class sessions, easily share code with students and save progress", + "description": "Homepage heading subtitle" + }, + "newpage-heading-title": { + "defaultMessage": "Welcome to micro:bit classroom", + "description": "Homepage heading title" + }, + "newpage-last-session-date": { + "defaultMessage": "Date: {date}", + "description": "Last session date label" + }, + "newpage-last-session-name": { + "defaultMessage": "Name: {name}", + "description": "Last session name label" + }, + "newpage-last-session-none": { + "defaultMessage": "No session found", + "description": "Last session not found text" + }, + "newpage-last-session-title": { + "defaultMessage": "Open last session", + "description": "Open last session title" + }, + "newpage-new-session-subtitle": { + "defaultMessage": "Connect your micro:bit and collect movement data to build a machine learning model", + "description": "Start new session subtitle" + }, + "newpage-new-session-title": { + "defaultMessage": "New session", + "description": "Start new session title" + }, + "newpage-section-one-title": { + "defaultMessage": "Pick up where you left off", + "description": "Homepage section one title" + }, + "newpage-section-two-title": { + "defaultMessage": "Start something new", + "description": "Homepage section two title" + }, + "newpage-subtitle": { + "defaultMessage": "Introduce students to machine learning concepts through physical movement and data", + "description": "Subtitle of micro:bit AI creator home page" + }, + "newpage-title": { + "defaultMessage": "New session", + "description": "New page title" + }, "no-data-samples": { "defaultMessage": "No data samples", "description": "Empty data samples page status text" @@ -867,6 +963,26 @@ "defaultMessage": "Start training", "description": "Start training button text" }, + "steps-code": { + "defaultMessage": "Code", + "description": "Step in home page diagram" + }, + "steps-collect-data": { + "defaultMessage": "Collect data", + "description": "Step in home page diagram" + }, + "steps-improve": { + "defaultMessage": "Improve", + "description": "Step in home page diagram" + }, + "steps-test-model": { + "defaultMessage": "Test model", + "description": "Step in home page diagram" + }, + "steps-train": { + "defaultMessage": "Train", + "description": "Step in home page diagram" + }, "support-request": { "defaultMessage": "Please consider raising a support request.", "description": "Support request link text" @@ -975,4 +1091,4 @@ "defaultMessage": "unknown", "description": "Label for unknown ML event" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 5349d84d7..3560be472 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,9 +21,14 @@ import { deployment, useDeployment } from "./deployment"; import { ProjectProvider } from "./hooks/project-hooks"; import { LoggingProvider } from "./logging/logging-hooks"; import TranslationProvider from "./messages/TranslationProvider"; -import HomePage from "./pages/HomePage"; -import { createHomePageUrl, createSessionPageUrl } from "./urls"; import { sessionPageConfigs } from "./pages-config"; +import HomePage from "./pages/HomePage"; +import NewPage from "./pages/NewPage"; +import { + createHomePageUrl, + createNewPageUrl, + createSessionPageUrl, +} from "./urls"; export interface ProviderLayoutProps { children: ReactNode; @@ -90,6 +95,10 @@ const createRouter = () => { path: createHomePageUrl(), element: , }, + { + path: createNewPageUrl(), + element: , + }, ...sessionPageConfigs.map((config) => { return { path: createSessionPageUrl(config.id), diff --git a/src/components/DefaultPageLayout.tsx b/src/components/DefaultPageLayout.tsx index ff0d067f6..d4c07b45b 100644 --- a/src/components/DefaultPageLayout.tsx +++ b/src/components/DefaultPageLayout.tsx @@ -11,15 +11,16 @@ import { VStack, } from "@chakra-ui/react"; import { ReactNode, useCallback, useEffect } from "react"; -import { RiDownload2Line, RiFolderOpenLine, RiHome2Line } from "react-icons/ri"; +import { RiDownload2Line, RiHome2Line } from "react-icons/ri"; import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate } from "react-router"; +import { useDeployment } from "../deployment"; import { flags } from "../flags"; import { useProject } from "../hooks/project-hooks"; -import { SaveStep, TrainModelDialogStage } from "../model"; +import { TrainModelDialogStage } from "../model"; import { SessionPageId } from "../pages-config"; import Tour from "../pages/Tour"; -import { useSettings, useStore } from "../store"; +import { useStore } from "../store"; import { createHomePageUrl, createSessionPageUrl } from "../urls"; import ActionBar from "./ActionBar"; import AppLogo from "./AppLogo"; @@ -27,41 +28,33 @@ import ConnectionDialogs from "./ConnectionFlowDialogs"; import DownloadDialogs from "./DownloadDialogs"; import HelpMenu from "./HelpMenu"; import LanguageMenuItem from "./LanguageMenuItem"; -import LoadProjectMenuItem from "./LoadProjectMenuItem"; -import OpenButton from "./OpenButton"; import PreReleaseNotice from "./PreReleaseNotice"; import SaveDialogs from "./SaveDialogs"; import SettingsMenu from "./SettingsMenu"; import ToolbarMenu from "./ToolbarMenu"; import TrainModelDialogs from "./TrainModelFlowDialogs"; -import { useDeployment } from "../deployment"; interface DefaultPageLayoutProps { titleId?: string; children: ReactNode; toolbarItemsLeft?: ReactNode; + toolbarItemsRight?: ReactNode; + menuItems?: ReactNode; showPageTitle?: boolean; - showHomeButton?: boolean; - showSaveButton?: boolean; - showOpenButton?: boolean; } const DefaultPageLayout = ({ titleId, children, + menuItems, toolbarItemsLeft, + toolbarItemsRight, showPageTitle = false, - showHomeButton = false, - showSaveButton = false, - showOpenButton = false, }: DefaultPageLayoutProps) => { const intl = useIntl(); const navigate = useNavigate(); const isEditorOpen = useStore((s) => s.isEditorOpen); const stage = useStore((s) => s.trainModelDialogStage); - - const { saveHex } = useProject(); - const [settings] = useSettings(); const toast = useToast(); const { appNameFull } = useDeployment(); @@ -91,19 +84,6 @@ const DefaultPageLayout = ({ ); }, [intl, navigate, toast]); - const handleHomeClick = useCallback(() => { - navigate(createHomePageUrl()); - }, [navigate]); - - const setSave = useStore((s) => s.setSave); - const handleSave = useCallback(() => { - if (settings.showPreSaveHelp) { - setSave({ step: SaveStep.PreSaveHelp }); - } else { - void saveHex(); - } - }, [saveHex, setSave, settings.showPreSaveHelp]); - return ( <> {/* Suppress dialogs to prevent overlapping dialogs */} @@ -138,26 +118,7 @@ const DefaultPageLayout = ({ itemsRight={ <> - {showOpenButton && } - {showSaveButton && ( - - )} - {showHomeButton && ( - } - aria-label={intl.formatMessage({ id: "homepage.Link" })} - variant="plain" - size="lg" - fontSize="xl" - /> - )} + {toolbarItemsRight} @@ -166,31 +127,7 @@ const DefaultPageLayout = ({ variant="plain" label={intl.formatMessage({ id: "main-menu" })} > - {showOpenButton && ( - } - accept=".hex" - > - - - )} - {showSaveButton && ( - } - > - - - )} - - {showHomeButton && ( - } - > - - - )} + {menuItems} @@ -205,4 +142,77 @@ const DefaultPageLayout = ({ ); }; +export const ProjectToolbarItems = () => { + const { saveHex } = useProject(); + const handleSave = useCallback(() => { + void saveHex(); + }, [saveHex]); + + return ( + <> + + + + ); +}; + +export const HomeToolbarItem = () => { + const intl = useIntl(); + const navigate = useNavigate(); + const handleHomeClick = useCallback(() => { + navigate(createHomePageUrl()); + }, [navigate]); + return ( + } + aria-label={intl.formatMessage({ id: "homepage.Link" })} + variant="plain" + size="lg" + fontSize="xl" + /> + ); +}; + +export const ProjectMenuItems = () => { + const { saveHex } = useProject(); + const handleSave = useCallback(() => { + void saveHex(); + }, [saveHex]); + + return ( + <> + } + > + + + + + + ); +}; + +export const HomeMenuItem = () => { + const navigate = useNavigate(); + const handleHomeClick = useCallback(() => { + navigate(createHomePageUrl()); + }, [navigate]); + return ( + } + > + + + ); +}; + export default DefaultPageLayout; diff --git a/src/components/LoadProjectInput.tsx b/src/components/LoadProjectInput.tsx index 1cd9e190b..fc78b1f17 100644 --- a/src/components/LoadProjectInput.tsx +++ b/src/components/LoadProjectInput.tsx @@ -8,7 +8,7 @@ export interface LoadProjectInputProps { * File input tag accept attribute. * A project can be opened from .json or .hex file. */ - accept?: ".json" | ".hex"; + accept: ".json" | ".hex" | ".json,.hex"; } export interface LoadProjectInputRef { diff --git a/src/components/LoadProjectMenuItem.tsx b/src/components/LoadProjectMenuItem.tsx index a26756780..a33f0c07d 100644 --- a/src/components/LoadProjectMenuItem.tsx +++ b/src/components/LoadProjectMenuItem.tsx @@ -13,7 +13,7 @@ interface LoadProjectMenuItemProps * File input tag accept attribute. * A project can be opened from .json or .hex file. */ - accept?: ".json" | ".hex"; + accept: ".json" | ".hex" | ".json,.hex"; } const LoadProjectMenuItem = ({ diff --git a/src/components/NewPageChoice.tsx b/src/components/NewPageChoice.tsx new file mode 100644 index 000000000..5d1ac20af --- /dev/null +++ b/src/components/NewPageChoice.tsx @@ -0,0 +1,77 @@ +import { + Box, + BoxProps, + Heading, + HStack, + IconButton, + Stack, +} from "@chakra-ui/react"; +import { ReactElement, ReactNode } from "react"; + +interface GetStartedChoiceProps extends BoxProps { + children: ReactNode; + onClick: () => void; + icon: ReactElement; + disabled?: boolean; + label: string; +} + +const NewPageChoice = ({ + disabled, + children, + onClick, + icon, + label, + ...props +}: GetStartedChoiceProps) => { + return ( + + + + {label} + + {children} + + + + + + ); +}; + +export default NewPageChoice; diff --git a/src/components/OpenButton.tsx b/src/components/OpenButton.tsx index 5f7db6aad..c11c74f3c 100644 --- a/src/components/OpenButton.tsx +++ b/src/components/OpenButton.tsx @@ -15,7 +15,7 @@ const OpenButton = () => { > - + ); }; diff --git a/src/components/PercentageDisplay.tsx b/src/components/PercentageDisplay.tsx index 67e5c1a67..5ce826756 100644 --- a/src/components/PercentageDisplay.tsx +++ b/src/components/PercentageDisplay.tsx @@ -1,6 +1,6 @@ -import { StackProps, Text } from "@chakra-ui/react"; +import { BoxProps, Text } from "@chakra-ui/react"; -interface PercentageDisplayProps extends StackProps { +interface PercentageDisplayProps extends BoxProps { value: number; colorScheme?: string; } @@ -8,6 +8,7 @@ interface PercentageDisplayProps extends StackProps { const PercentageDisplay = ({ value, colorScheme = "gray.600", + ...rest }: PercentageDisplayProps) => { return ( {`${Math.round(value * 100)}%`} ); }; diff --git a/src/components/RecordingGraph.tsx b/src/components/RecordingGraph.tsx index 1326b6c65..29d3e4281 100644 --- a/src/components/RecordingGraph.tsx +++ b/src/components/RecordingGraph.tsx @@ -1,4 +1,4 @@ -import { Box } from "@chakra-ui/react"; +import { Box, BoxProps } from "@chakra-ui/react"; import { Chart, LineController, @@ -11,11 +11,11 @@ import { useEffect, useRef } from "react"; import { XYZData } from "../model"; import { getConfig as getRecordingChartConfig } from "../recording-graph"; -interface RecordingGraphProps { +interface RecordingGraphProps extends BoxProps { data: XYZData; } -const RecordingGraph = ({ data }: RecordingGraphProps) => { +const RecordingGraph = ({ data, ...rest }: RecordingGraphProps) => { const canvasRef = useRef(null); useEffect(() => { @@ -40,6 +40,7 @@ const RecordingGraph = ({ data }: RecordingGraphProps) => { borderColor="gray.200" width="100%" height="100%" + {...rest} > diff --git a/src/components/ResourceCard.tsx b/src/components/ResourceCard.tsx new file mode 100644 index 000000000..b69017952 --- /dev/null +++ b/src/components/ResourceCard.tsx @@ -0,0 +1,46 @@ +import { + AspectRatio, + HStack, + Heading, + Image, + LinkBox, + LinkOverlay, + VStack, +} from "@chakra-ui/react"; +import { ReactNode } from "react"; +import Link from "./Link"; + +interface ResourceCardProps { + url: string; + imgSrc: string; + title: ReactNode; +} + +const ResourceCard = ({ imgSrc, url, title }: ResourceCardProps) => { + return ( + + + + + + + + + {title} + + + + + + ); +}; + +export default ResourceCard; diff --git a/src/components/ResourceCardPlaceholder.tsx b/src/components/ResourceCardPlaceholder.tsx new file mode 100644 index 000000000..47f5cf93b --- /dev/null +++ b/src/components/ResourceCardPlaceholder.tsx @@ -0,0 +1,34 @@ +import { AspectRatio, Box, Text, VStack } from "@chakra-ui/react"; + +const ResourceCardPlaceholder = () => { + return ( + + + + + + + Coming soon + + + ); +}; + +export default ResourceCardPlaceholder; diff --git a/src/components/StartOverWarningDialog.tsx b/src/components/StartOverWarningDialog.tsx deleted file mode 100644 index 9ddaafe75..000000000 --- a/src/components/StartOverWarningDialog.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - Button, - Heading, - Link, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalOverlay, - Text, - VStack, -} from "@chakra-ui/react"; -import { ReactNode, useCallback } from "react"; -import { FormattedMessage } from "react-intl"; -import { useProject } from "../hooks/project-hooks"; - -interface StartOverWardningDialogProps { - isOpen: boolean; - onClose: () => void; - onStart: () => void; -} - -const StartOverWarningDialog = ({ - isOpen, - onClose, - onStart, -}: StartOverWardningDialogProps) => { - const { saveHex } = useProject(); - const handleSaveHex = useCallback(async () => { - await saveHex(); - }, [saveHex]); - return ( - - - - - - - - - - - - - - - ( - - {chunks} - - ), - }} - /> - - - - - - - - - - - ); -}; - -export default StartOverWarningDialog; diff --git a/src/components/StartResumeActions.tsx b/src/components/StartResumeActions.tsx deleted file mode 100644 index 24150d775..000000000 --- a/src/components/StartResumeActions.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Button, HStack, StackProps, useDisclosure } from "@chakra-ui/react"; -import { useCallback } from "react"; -import { FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router"; -import { useConnectionStage } from "../connection-stage-hooks"; -import { SessionPageId } from "../pages-config"; -import { useHasGestures, useStore } from "../store"; -import { createSessionPageUrl } from "../urls"; -import StartOverWarningDialog from "./StartOverWarningDialog"; - -const StartResumeActions = ({ ...props }: Partial) => { - const newSession = useStore((s) => s.newSession); - const hasExistingSession = useHasGestures(); - const startOverWarningDialogDisclosure = useDisclosure(); - const navigate = useNavigate(); - const { actions: connStageActions } = useConnectionStage(); - - const handleNavigateToAddData = useCallback(() => { - navigate(createSessionPageUrl(SessionPageId.DataSamples)); - }, [navigate]); - - const handleStartNewSession = useCallback(() => { - startOverWarningDialogDisclosure.onClose(); - newSession(); - handleNavigateToAddData(); - connStageActions.startConnect(); - }, [ - startOverWarningDialogDisclosure, - newSession, - handleNavigateToAddData, - connStageActions, - ]); - - const onClickStartNewSession = useCallback(() => { - if (hasExistingSession) { - startOverWarningDialogDisclosure.onOpen(); - } else { - handleStartNewSession(); - } - }, [ - handleStartNewSession, - hasExistingSession, - startOverWarningDialogDisclosure, - ]); - - return ( - <> - - - {hasExistingSession && ( - - )} - - - - ); -}; - -export default StartResumeActions; diff --git a/src/components/YoutubeVideoEmbed.tsx b/src/components/YoutubeVideoEmbed.tsx new file mode 100644 index 000000000..8df16fb07 --- /dev/null +++ b/src/components/YoutubeVideoEmbed.tsx @@ -0,0 +1,34 @@ +import { AspectRatio, Box } from "@chakra-ui/react"; + +export interface YoutubeVideo { + alt: string; + youtubeId: string; +} + +interface YoutubeVideoProps { + alt: string; + youtubeId: string; +} + +const YoutubeVideoEmbed = ({ alt, youtubeId }: YoutubeVideoProps) => { + return ( + + +