From eef463acace6a648d3494729977292ac83bd3cb9 Mon Sep 17 00:00:00 2001 From: AlexandreSi <32449369+AlexandreSi@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:14:50 +0200 Subject: [PATCH] Add possibility to open tutorial template --- .../MainFrame/EditorContainers/BaseEditor.js | 1 + .../LearnSection/EducationCurriculumLesson.js | 36 +++++++++++++++---- .../LearnSection/TutorialsCategoryPage.js | 20 +++++++++-- .../HomePage/LearnSection/index.js | 3 ++ .../EditorContainers/HomePage/index.js | 4 +++ newIDE/app/src/MainFrame/index.js | 30 ++++++++++++++++ .../src/Utils/GDevelopServices/Tutorial.js | 5 +-- newIDE/app/src/Utils/UseCreateProject.js | 30 ++++++++++++++++ .../GDevelopServicesTestData/FakeTutorials.js | 2 ++ .../HomePage/HomePage.stories.js | 3 ++ .../HomePage/LearnSection.stories.js | 8 ++++- 11 files changed, 130 insertions(+), 12 deletions(-) diff --git a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js index 84ebd0025999..3eb5f04a4a2b 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js +++ b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js @@ -112,6 +112,7 @@ export type RenderEditorContainerProps = {| newProjectSetup: NewProjectSetup, i18n: I18nType ) => Promise, + onOpenTemplateFromTutorial: (tutorialId: string) => Promise, // Project save onSave: () => Promise, diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/EducationCurriculumLesson.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/EducationCurriculumLesson.js index 51216b395c42..b39451f11f55 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/EducationCurriculumLesson.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/EducationCurriculumLesson.js @@ -99,6 +99,7 @@ type Props = {| tutorial: Tutorial, onSelectTutorial: (tutorial: Tutorial) => void, index: number, + onOpenTemplateFromTutorial: ?(string) => void, |}; const EducationCurriculumLesson = ({ @@ -107,6 +108,7 @@ const EducationCurriculumLesson = ({ limits, onSelectTutorial, index, + onOpenTemplateFromTutorial, }: Props) => { const { isMobile } = useResponsiveWindowSize(); const [isImageLoaded, setIsImageLoaded] = React.useState(false); @@ -168,6 +170,14 @@ const EducationCurriculumLesson = ({ {!isMobile && title} + {gameLink && isMobile && !isUpcomingMessage && !isLocked && ( + } + label={Play game} + onClick={() => Window.openExternalURL(gameLink)} + /> + )}
{tutorial.tagsByLocale && tutorial.tagsByLocale.map(tagByLocale => { @@ -192,8 +202,9 @@ const EducationCurriculumLesson = ({ noMargin alignItems="center" justifyContent={gameLink ? 'space-between' : 'flex-end'} + expand={isMobile} > - {gameLink && ( + {gameLink && !isMobile && ( } @@ -201,12 +212,23 @@ const EducationCurriculumLesson = ({ onClick={() => Window.openExternalURL(gameLink)} /> )} - Open lesson} - onClick={() => onSelectTutorial(tutorial)} - /> + + {onOpenTemplateFromTutorial && ( + Open project} + onClick={onOpenTemplateFromTutorial} + /> + )} + Open lesson} + onClick={() => onSelectTutorial(tutorial)} + /> + )} diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/TutorialsCategoryPage.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/TutorialsCategoryPage.js index 25aff2319a16..2a70923523c2 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/TutorialsCategoryPage.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/LearnSection/TutorialsCategoryPage.js @@ -31,6 +31,7 @@ type EducationCurriculumProps = {| limits: ?Limits, tutorials: Tutorial[], onSelectTutorial: Tutorial => void, + onOpenTemplateFromTutorial: string => Promise, |}; const EducationCurriculum = ({ @@ -38,6 +39,7 @@ const EducationCurriculum = ({ limits, tutorials, onSelectTutorial, + onOpenTemplateFromTutorial, }: EducationCurriculumProps) => { const listItems: React.Node[] = React.useMemo( () => { @@ -67,13 +69,20 @@ const EducationCurriculum = ({ tutorial={tutorial} onSelectTutorial={onSelectTutorial} index={sectionIndex} + onOpenTemplateFromTutorial={ + tutorial.templateUrl + ? () => { + onOpenTemplateFromTutorial(tutorial.id); + } + : null + } /> ); sectionIndex += 1; }); return items; }, - [tutorials, i18n, limits, onSelectTutorial] + [tutorials, i18n, limits, onSelectTutorial, onOpenTemplateFromTutorial] ); return ( @@ -100,9 +109,15 @@ type Props = {| onBack: () => void, tutorials: Array, category: TutorialCategory, + onOpenTemplateFromTutorial: string => Promise, |}; -const TutorialsCategoryPage = ({ category, tutorials, onBack }: Props) => { +const TutorialsCategoryPage = ({ + category, + tutorials, + onBack, + onOpenTemplateFromTutorial, +}: Props) => { const { limits } = React.useContext(AuthenticatedUserContext); const texts = TUTORIAL_CATEGORY_TEXTS[category]; const filteredTutorials = tutorials.filter( @@ -129,6 +144,7 @@ const TutorialsCategoryPage = ({ category, tutorials, onBack }: Props) => { onSelectTutorial={setSelectedTutorial} i18n={i18n} limits={limits} + onOpenTemplateFromTutorial={onOpenTemplateFromTutorial} /> ) : ( void, selectInAppTutorial: (tutorialId: string) => void, initialCategory: TutorialCategory | null, + onOpenTemplateFromTutorial: string => Promise, |}; const LearnSection = ({ @@ -126,6 +127,7 @@ const LearnSection = ({ onTabChange, selectInAppTutorial, initialCategory, + onOpenTemplateFromTutorial, }: Props) => { const { tutorials, @@ -181,6 +183,7 @@ const LearnSection = ({ onBack={() => setSelectedCategory(null)} category={selectedCategory} tutorials={tutorials} + onOpenTemplateFromTutorial={onOpenTemplateFromTutorial} /> ); }; diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js index e0b5655b233d..cfccfbef45a2 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js @@ -127,6 +127,7 @@ type Props = {| newProjectSetup: NewProjectSetup, i18n: I18nType ) => Promise, + onOpenTemplateFromTutorial: (tutorialId: string) => Promise, // Project save onSave: () => Promise, @@ -174,6 +175,7 @@ export const HomePage = React.memo( canSave, resourceManagementProps, askToCloseProject, + onOpenTemplateFromTutorial, }: Props, ref ) => { @@ -528,6 +530,7 @@ export const HomePage = React.memo( onOpenExampleStore={onOpenExampleStore} onTabChange={setActiveTab} selectInAppTutorial={selectInAppTutorial} + onOpenTemplateFromTutorial={onOpenTemplateFromTutorial} initialCategory={learnInitialCategory} /> )} @@ -638,6 +641,7 @@ export const renderHomePageContainer = ( } onOpenNewProjectSetupDialog={props.onOpenNewProjectSetupDialog} onOpenProjectManager={props.onOpenProjectManager} + onOpenTemplateFromTutorial={props.onOpenTemplateFromTutorial} onOpenLanguageDialog={props.onOpenLanguageDialog} onOpenProfile={props.onOpenProfile} onCreateProjectFromExample={props.onCreateProjectFromExample} diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index d6b35a11c00e..919b7f36eb10 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -1138,6 +1138,7 @@ const MainFrame = (props: Props) => { createProjectFromExample, createProjectFromPrivateGameTemplate, createProjectFromInAppTutorial, + createProjectFromTutorial, createProjectWithLogin, createProjectFromAIGeneration, } = useCreateProject({ @@ -2984,6 +2985,34 @@ const MainFrame = (props: Props) => { } }; + const openTemplateFromTutorial = React.useCallback( + async (tutorialId: string) => { + const projectIsClosed = await askToCloseProject(); + if (!projectIsClosed) { + return; + } + try { + await createProjectFromTutorial(tutorialId, { + storageProvider: emptyStorageProvider, + saveAsLocation: null, + // Remaining will be set by the template. + }); + } catch (error) { + showErrorBox({ + message: i18n._( + t`Unable to create a new project for the tutorial. Try again later.` + ), + rawError: new Error( + `Can't create project from template of tutorial "${tutorialId}"` + ), + errorId: 'cannot-create-project-from-tutorial-template', + }); + return; + } + }, + [askToCloseProject, createProjectFromTutorial, i18n] + ); + const startSelectedTutorial = React.useCallback( async (scenario: 'resume' | 'startOver' | 'start') => { if (!selectedInAppTutorialInfo) return; @@ -3474,6 +3503,7 @@ const MainFrame = (props: Props) => { openSceneEditor: false, }); }, + onOpenTemplateFromTutorial: openTemplateFromTutorial, previewDebuggerServer, hotReloadPreviewButtonProps, onOpenLayout: name => { diff --git a/newIDE/app/src/Utils/GDevelopServices/Tutorial.js b/newIDE/app/src/Utils/GDevelopServices/Tutorial.js index ffd709ba6800..7fadb58efb23 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Tutorial.js +++ b/newIDE/app/src/Utils/GDevelopServices/Tutorial.js @@ -34,10 +34,11 @@ export type Tutorial = {| isPrivateTutorial?: boolean, redeemHintByLocale?: MessageByLocale, redeemLinkByLocale?: MessageByLocale, - sectionByLocale?: MessageByLocale; - tagsByLocale?: MessageByLocale[]; + sectionByLocale?: MessageByLocale, + tagsByLocale?: MessageByLocale[], availableAt?: string, gameLink?: string, + templateUrl?: string, |}; export const canAccessTutorial = ( diff --git a/newIDE/app/src/Utils/UseCreateProject.js b/newIDE/app/src/Utils/UseCreateProject.js index a2b8f39c6da0..8a7f4b123815 100644 --- a/newIDE/app/src/Utils/UseCreateProject.js +++ b/newIDE/app/src/Utils/UseCreateProject.js @@ -33,6 +33,7 @@ import { } from './GDevelopServices/Shop'; import { createPrivateGameTemplateUrl } from './GDevelopServices/Asset'; import { getDefaultRegisterGamePropertiesFromProject } from './UseGameAndBuildsManager'; +import { TutorialContext } from '../Tutorial/TutorialContext'; type Props = {| beforeCreatingProject: () => void, @@ -80,6 +81,7 @@ const useCreateProject = ({ const { getInAppTutorialShortHeader } = React.useContext( InAppTutorialContext ); + const { tutorials } = React.useContext(TutorialContext); const initialiseProjectProperties = ( project: gdProject, @@ -346,6 +348,33 @@ const useCreateProject = ({ [beforeCreatingProject, createProject, getInAppTutorialShortHeader] ); + const createProjectFromTutorial = React.useCallback( + async (tutorialId: string, newProjectSetup: NewProjectSetup) => { + beforeCreatingProject(); + if (!tutorials) { + throw new Error(`Tutorials could not be loaded`); + } + const selectedTutorial = tutorials.find( + tutorial => tutorial.id === tutorialId + ); + if (!selectedTutorial) { + throw new Error(`No tutorial found for id "${tutorialId}"`); + } + const { templateUrl } = selectedTutorial; + if (!templateUrl) { + throw new Error(`No template URL for the tutorial "${tutorialId}"`); + } + const newProjectSource = await createNewProjectFromTutorialTemplate( + templateUrl, + tutorialId + ); + await createProject(newProjectSource, newProjectSetup, { + openAllScenes: true, + }); + }, + [beforeCreatingProject, createProject, tutorials] + ); + const createProjectWithLogin = React.useCallback( async (newProjectSetup: NewProjectSetup) => { beforeCreatingProject(); @@ -371,6 +400,7 @@ const useCreateProject = ({ createProjectFromExample, createProjectFromPrivateGameTemplate, createProjectFromInAppTutorial, + createProjectFromTutorial, createProjectWithLogin, createProjectFromAIGeneration, }; diff --git a/newIDE/app/src/fixtures/GDevelopServicesTestData/FakeTutorials.js b/newIDE/app/src/fixtures/GDevelopServicesTestData/FakeTutorials.js index 2ccc761b53ac..358115536446 100644 --- a/newIDE/app/src/fixtures/GDevelopServicesTestData/FakeTutorials.js +++ b/newIDE/app/src/fixtures/GDevelopServicesTestData/FakeTutorials.js @@ -76,6 +76,8 @@ export const fakeTutorials: Array = [ tagsByLocale: [{ en: 'Single player' }, { en: 'Beginner' }], sectionByLocale: { en: 'Practical lessons' }, gameLink: 'https://gd.games/gdevelop/flappy-cat', + templateUrl: + 'https://resources.gdevelop-app.com/tutorials/templates/flappy-cat/game.json', }, { id: 'education-curriculum-angry-pigs', diff --git a/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js b/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js index 9fb3ea56f786..20f56802596c 100644 --- a/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js +++ b/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js @@ -108,6 +108,9 @@ const WrappedHomePage = ({ onOpenNewProjectSetupDialog={() => action('onOpenNewProjectSetupDialog')() } + onOpenTemplateFromTutorial={() => + action('onOpenTemplateFromTutorial')() + } canSave={true} onSave={() => action('onSave')()} selectInAppTutorial={() => action('select in app tutorial')()} diff --git a/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js b/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js index 90eab87f4a9e..1c064c09f242 100644 --- a/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js +++ b/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js @@ -24,7 +24,9 @@ export default { }; export const Default = () => ( - + ( onOpenExampleStore={action('onOpenExampleStore')} onTabChange={() => {}} selectInAppTutorial={action('selectInAppTutorial')} + onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} /> @@ -61,6 +64,7 @@ export const EducationSubscriber = () => ( onOpenExampleStore={action('onOpenExampleStore')} onTabChange={() => {}} selectInAppTutorial={action('selectInAppTutorial')} + onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} /> @@ -84,6 +88,7 @@ export const EducationTeacher = () => ( onOpenExampleStore={action('onOpenExampleStore')} onTabChange={() => {}} selectInAppTutorial={action('selectInAppTutorial')} + onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} /> @@ -104,6 +109,7 @@ export const Loading = () => ( onOpenExampleStore={action('onOpenExampleStore')} onTabChange={() => {}} selectInAppTutorial={action('selectInAppTutorial')} + onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} />