From 76eaa747c95d63aedcb7e2b5433eda7fe328a25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:20:58 +0100 Subject: [PATCH] Rework Game Creation dialog (#7179) * It now becomes the entry point for creating a new game suggesting: * the empty project option (as before) * NEW: starting points for your projects, to bootstrap your platformer, top-down or physics game. They are extremely simple template, that can be used to avoid starting from scratch, which can be scary & difficult. * the list of finished games that be remixed into your own, with a search. --- .../AssetStore/ExampleStore/ExampleDialog.js | 191 ----- .../ExampleStore/ExampleInformationPage.js | 112 +++ .../ExampleStore/ExampleListItem.js | 174 ----- .../ExampleStore/ExampleStoreDialog.js | 86 -- .../ExampleStore/ExampleThumbnailOrIcon.js | 10 +- .../app/src/AssetStore/ExampleStore/index.js | 334 ++++---- .../PrivateGameTemplateInformationDialog.js | 52 -- .../PrivateGameTemplateInformationPage.js | 32 +- .../PrivateGameTemplateListItem.js | 176 ----- ...PrivateGameTemplateOwnedInformationPage.js | 93 +++ .../PrivateGameTemplateThumbnail.js | 59 ++ newIDE/app/src/AssetStore/ShopTiles.js | 129 ++- .../MainFrame/EditorContainers/BaseEditor.js | 7 +- .../HomePage/BuildSection/index.js | 76 +- .../HomePage/BuildSection/utils.js | 307 +++++--- .../HomePage/LearnSection/MainPage.js | 7 - .../HomePage/LearnSection/index.js | 3 - .../EditorContainers/HomePage/index.js | 17 +- newIDE/app/src/MainFrame/ElectronMainMenu.js | 7 +- newIDE/app/src/MainFrame/MainFrameCommands.js | 4 +- newIDE/app/src/MainFrame/MainMenu.js | 25 +- newIDE/app/src/MainFrame/RouterContext.js | 3 +- .../UseExampleOrGameTemplateDialogs.js | 208 ----- .../app/src/MainFrame/UseNewProjectDialog.js | 241 ++++++ newIDE/app/src/MainFrame/index.js | 96 +-- .../app/src/ProjectCreation/AIPromptField.js | 210 +++++ .../app/src/ProjectCreation/CreateProject.js | 25 - .../EmptyAndStartingPointProjects.js | 154 ++++ .../EmptyAndStartingPointProjects.module.css | 27 + .../ProjectCreation/NewProjectSetupDialog.js | 739 +++++++++--------- .../QuickCustomizationGameTiles.js | 2 +- newIDE/app/src/UI/Dialog.js | 39 +- newIDE/app/src/UI/Layout.js | 12 +- .../app/src/UI/Theme/BlueDarkTheme/theme.json | 7 +- .../src/UI/Theme/DefaultDarkTheme/theme.json | 7 +- .../src/UI/Theme/DefaultLightTheme/theme.json | 7 +- newIDE/app/src/UI/Theme/NordTheme/theme.json | 7 +- .../app/src/UI/Theme/OneDarkTheme/theme.json | 7 +- .../app/src/UI/Theme/RosePineTheme/theme.json | 7 +- .../UI/Theme/SolarizedDarkTheme/theme.json | 7 +- .../app/src/Utils/GDevelopServices/Example.js | 2 +- newIDE/app/src/Utils/UseCreateProject.js | 11 - .../GDevelopServicesTestData/index.js | 2 + .../ExampleStore/ExampleDialog.stories.js | 22 - .../ExampleStore/ExampleStore.stories.js | 31 - .../ExampleStoreDialog.stories.js | 29 - .../HomePage/HomePage.stories.js | 5 +- .../HomePage/LearnSection.stories.js | 5 - .../NewProjectSetupDialog.stories.js | 331 +++++--- 49 files changed, 2116 insertions(+), 2028 deletions(-) delete mode 100644 newIDE/app/src/AssetStore/ExampleStore/ExampleDialog.js create mode 100644 newIDE/app/src/AssetStore/ExampleStore/ExampleInformationPage.js delete mode 100644 newIDE/app/src/AssetStore/ExampleStore/ExampleListItem.js delete mode 100644 newIDE/app/src/AssetStore/ExampleStore/ExampleStoreDialog.js delete mode 100644 newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog.js delete mode 100644 newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateListItem.js create mode 100644 newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateOwnedInformationPage.js create mode 100644 newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateThumbnail.js delete mode 100644 newIDE/app/src/MainFrame/UseExampleOrGameTemplateDialogs.js create mode 100644 newIDE/app/src/MainFrame/UseNewProjectDialog.js create mode 100644 newIDE/app/src/ProjectCreation/AIPromptField.js create mode 100644 newIDE/app/src/ProjectCreation/EmptyAndStartingPointProjects.js create mode 100644 newIDE/app/src/ProjectCreation/EmptyAndStartingPointProjects.module.css delete mode 100644 newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleDialog.stories.js delete mode 100644 newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStore.stories.js delete mode 100644 newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStoreDialog.stories.js diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleDialog.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleDialog.js deleted file mode 100644 index 96d5c9febed8..000000000000 --- a/newIDE/app/src/AssetStore/ExampleStore/ExampleDialog.js +++ /dev/null @@ -1,191 +0,0 @@ -// @flow -import { t } from '@lingui/macro'; -import { Trans } from '@lingui/macro'; -import * as React from 'react'; -import Dialog from '../../UI/Dialog'; -import FlatButton from '../../UI/FlatButton'; -import { - type ExampleShortHeader, - type Example, - getExample, -} from '../../Utils/GDevelopServices/Example'; -import { isCompatibleWithAsset } from '../../Utils/GDevelopServices/Asset'; -import PlaceholderError from '../../UI/PlaceholderError'; -import { MarkdownText } from '../../UI/MarkdownText'; -import Text from '../../UI/Text'; -import AlertMessage from '../../UI/AlertMessage'; -import { getIDEVersion } from '../../Version'; -import { Column, Line } from '../../UI/Grid'; -import Divider from '@material-ui/core/Divider'; -import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout'; -import { ExampleThumbnailOrIcon } from './ExampleThumbnailOrIcon'; -import RaisedButtonWithSplitMenu from '../../UI/RaisedButtonWithSplitMenu'; -import Window from '../../Utils/Window'; -import optionalRequire from '../../Utils/OptionalRequire'; -import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip'; -import { ExampleDifficultyChip } from '../../UI/ExampleDifficultyChip'; -import { ExampleSizeChip } from '../../UI/ExampleSizeChip'; -const isDev = Window.isDev(); - -const electron = optionalRequire('electron'); - -type Props = {| - exampleShortHeader: ExampleShortHeader, - isOpening: boolean, - onClose: () => void, - onOpen: () => void, -|}; - -export const openExampleInWebApp = (example: Example) => { - Window.openExternalURL( - `${ - isDev ? 'http://localhost:3000' : 'https://editor.gdevelop.io' - }/?create-from-example=${example.slug}` - ); -}; - -export function ExampleDialog({ - isOpening, - exampleShortHeader, - onClose, - onOpen, -}: Props) { - const [error, setError] = React.useState(null); - const [example, setExample] = React.useState(null); - - const loadExample = React.useCallback( - async () => { - setError(null); - try { - const example = await getExample(exampleShortHeader); - setExample(example); - } catch (error) { - setError(error); - } - }, - [exampleShortHeader] - ); - - React.useEffect( - () => { - loadExample(); - }, - [loadExample] - ); - - const isCompatible = isCompatibleWithAsset( - getIDEVersion(), - exampleShortHeader - ); - const hasIcon = exampleShortHeader.previewImageUrls.length > 0; - - const canOpenExample = !isOpening && isCompatible; - const onOpenExample = React.useCallback( - () => { - if (canOpenExample) onOpen(); - }, - [onOpen, canOpenExample] - ); - - return ( - Back} - primary={false} - onClick={onClose} - disabled={isOpening} - />, - Not compatible : Open - } - primary - onClick={onOpenExample} - disabled={!canOpenExample || isOpening} - buildMenuTemplate={i18n => [ - { - label: electron - ? i18n._(t`Open in the web-app`) - : i18n._(t`Open in a new tab`), - disabled: !example, - click: () => { - if (example) openExampleInWebApp(example); - }, - }, - ]} - />, - ]} - open - cannotBeDismissed={isOpening} - onRequestClose={onClose} - onApply={onOpenExample} - > - - {!isCompatible && ( - - - Unfortunately, this example requires a newer version of GDevelop - to work. Update GDevelop to be able to open this example. - - - )} - - {hasIcon ? ( - - ) : null} - - { - -
- {exampleShortHeader.difficultyLevel && ( - - )} - {exampleShortHeader.codeSizeLevel && ( - - )} - {exampleShortHeader.authors && - exampleShortHeader.authors.map(author => ( - - ))} -
-
- } - {exampleShortHeader.shortDescription} -
-
- - {example && example.description && ( - - - - - - - )} - {!example && error && ( - - - Can't load the example. Verify your internet connection or try - again later. - - - )} -
-
- ); -} diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleInformationPage.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleInformationPage.js new file mode 100644 index 000000000000..d7cbeb9650f1 --- /dev/null +++ b/newIDE/app/src/AssetStore/ExampleStore/ExampleInformationPage.js @@ -0,0 +1,112 @@ +// @flow +import { Trans } from '@lingui/macro'; +import * as React from 'react'; +import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example'; +import { isCompatibleWithAsset } from '../../Utils/GDevelopServices/Asset'; +import { MarkdownText } from '../../UI/MarkdownText'; +import Text from '../../UI/Text'; +import AlertMessage from '../../UI/AlertMessage'; +import { getIDEVersion } from '../../Version'; +import { Column, Line } from '../../UI/Grid'; +import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout'; +import { ExampleThumbnailOrIcon } from './ExampleThumbnailOrIcon'; +import Window from '../../Utils/Window'; +import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip'; +import { ExampleDifficultyChip } from '../../UI/ExampleDifficultyChip'; +import { ExampleSizeChip } from '../../UI/ExampleSizeChip'; +import { + isStartingPointExampleShortHeader, + getStartingPointExampleShortHeaderTitle, +} from '../../ProjectCreation/EmptyAndStartingPointProjects'; +const isDev = Window.isDev(); + +const styles = { + chipsContainer: { + display: 'flex', + flexWrap: 'wrap', + }, +}; + +type Props = {| + exampleShortHeader: ExampleShortHeader, +|}; + +export const openExampleInWebApp = (exampleShortHeader: ExampleShortHeader) => { + Window.openExternalURL( + `${ + isDev ? 'http://localhost:3000' : 'https://editor.gdevelop.io' + }/?create-from-example=${exampleShortHeader.slug}` + ); +}; + +const getExampleName = (exampleShortHeader: ExampleShortHeader) => { + return isStartingPointExampleShortHeader(exampleShortHeader) + ? getStartingPointExampleShortHeaderTitle(exampleShortHeader) + : exampleShortHeader.name; +}; + +const ExampleInformationPage = ({ exampleShortHeader }: Props) => { + const isCompatible = isCompatibleWithAsset( + getIDEVersion(), + exampleShortHeader + ); + const hasIcon = exampleShortHeader.previewImageUrls.length > 0; + + return ( + + {!isCompatible && ( + + + Download the latest version of GDevelop to check out this example! + + + )} + + {hasIcon ? ( + + + + ) : null} + + +
+ {exampleShortHeader.difficultyLevel && ( + + )} + {exampleShortHeader.codeSizeLevel && ( + + )} + {exampleShortHeader.authors && + exampleShortHeader.authors.map(author => ( + + ))} +
+
+ + + {getExampleName(exampleShortHeader)} + + + + + + {exampleShortHeader.description && ( + + + + )} +
+
+
+ ); +}; + +export default ExampleInformationPage; diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleListItem.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleListItem.js deleted file mode 100644 index 1499cd9beb39..000000000000 --- a/newIDE/app/src/AssetStore/ExampleStore/ExampleListItem.js +++ /dev/null @@ -1,174 +0,0 @@ -// @flow -import { t } from '@lingui/macro'; -import { type I18n as I18nType } from '@lingui/core'; -import * as React from 'react'; -import { - type ExampleShortHeader, - getExample, -} from '../../Utils/GDevelopServices/Example'; -import { isCompatibleWithAsset } from '../../Utils/GDevelopServices/Asset'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import Text from '../../UI/Text'; -import { Trans } from '@lingui/macro'; -import { Column, Line } from '../../UI/Grid'; -import FlatButtonWithSplitMenu from '../../UI/FlatButtonWithSplitMenu'; -import { getIDEVersion } from '../../Version'; -import { ExampleThumbnailOrIcon } from './ExampleThumbnailOrIcon'; -import optionalRequire from '../../Utils/OptionalRequire'; -import { openExampleInWebApp } from './ExampleDialog'; -import { UserPublicProfileChip } from '../../UI/User/UserPublicProfileChip'; -import { ExampleSizeChip } from '../../UI/ExampleSizeChip'; -import { ExampleDifficultyChip } from '../../UI/ExampleDifficultyChip'; -import HighlightedText from '../../UI/Search/HighlightedText'; -import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem'; -import { ResponsiveLineStackLayout } from '../../UI/Layout'; -import useAlertDialog from '../../UI/Alert/useAlertDialog'; - -const electron = optionalRequire('electron'); - -const styles = { - container: { - display: 'flex', - overflow: 'hidden', - paddingTop: 8, - paddingBottom: 8, - paddingRight: 8, - }, - button: { - alignItems: 'flex-start', - textAlign: 'left', - flex: 1, - }, -}; - -type Props = {| - exampleShortHeader: ExampleShortHeader, - matches: ?Array, - isOpening: boolean, - onChoose: () => void, - onOpen: () => void, - onHeightComputed: number => void, -|}; - -const ExampleListItem = ({ - exampleShortHeader, - matches, - isOpening, - onChoose, - onOpen, - onHeightComputed, -}: Props) => { - const { showAlert } = useAlertDialog(); - // Report the height of the item once it's known. - const containerRef = React.useRef(null); - React.useLayoutEffect(() => { - if (containerRef.current) - onHeightComputed(containerRef.current.getBoundingClientRect().height); - }); - - const isCompatible = isCompatibleWithAsset( - getIDEVersion(), - exampleShortHeader - ); - - const fetchAndOpenExampleInWebApp = React.useCallback( - async (i18n: I18nType) => { - try { - const example = await getExample(exampleShortHeader); - openExampleInWebApp(example); - } catch (error) { - await showAlert({ - title: t`Unable to fetch the example.`, - message: t`Verify your internet connection or try again later.`, - }); - } - }, - [exampleShortHeader, showAlert] - ); - - const renderExampleField = (field: 'shortDescription' | 'name') => { - const originalField = exampleShortHeader[field]; - - if (!matches) return originalField; - const nameMatches = matches.filter(match => match.key === field); - if (nameMatches.length === 0) return originalField; - - return ( - - ); - }; - - return ( -
- - - - {!!exampleShortHeader.previewImageUrls.length && ( - - - - )} - - {renderExampleField('name')} - -
- {exampleShortHeader.difficultyLevel && ( - - )} - {exampleShortHeader.codeSizeLevel && ( - - )} - {exampleShortHeader.authors && - exampleShortHeader.authors.map(author => ( - - ))} -
-
- - {renderExampleField('shortDescription')} - -
-
-
- - - Open} - disabled={isOpening || !isCompatible} - onClick={onOpen} - buildMenuTemplate={i18n => [ - { - label: i18n._(t`Open details`), - click: onChoose, - }, - { - label: electron - ? i18n._(t`Open in the web-app`) - : i18n._(t`Open in a new tab`), - click: () => { - fetchAndOpenExampleInWebApp(i18n); - }, - }, - ]} - /> - - -
-
- ); -}; - -export default ExampleListItem; diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleStoreDialog.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleStoreDialog.js deleted file mode 100644 index 1c56430e812e..000000000000 --- a/newIDE/app/src/AssetStore/ExampleStore/ExampleStoreDialog.js +++ /dev/null @@ -1,86 +0,0 @@ -// @flow -import { Trans } from '@lingui/macro'; -import { I18n } from '@lingui/react'; -import * as React from 'react'; -import { ExampleStore } from '../../AssetStore/ExampleStore'; -import Dialog, { DialogPrimaryButton } from '../../UI/Dialog'; -import FlatButton from '../../UI/FlatButton'; -import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example'; -import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; - -export type ExampleStoreDialogProps = {| - open: boolean, - onClose: () => void, - selectedExampleShortHeader: ?ExampleShortHeader, - selectedPrivateGameTemplateListingData: ?PrivateGameTemplateListingData, - onSelectExampleShortHeader: (exampleShortHeader: ?ExampleShortHeader) => void, - onSelectPrivateGameTemplateListingData: ( - privateGameTemplateListingData: ?PrivateGameTemplateListingData - ) => void, - onOpenNewProjectSetupDialog: () => void, - isProjectOpening: boolean, -|}; - -const ExampleStoreDialog = ({ - open, - onClose, - selectedExampleShortHeader, - selectedPrivateGameTemplateListingData, - onSelectExampleShortHeader, - onSelectPrivateGameTemplateListingData, - onOpenNewProjectSetupDialog, - isProjectOpening, -}: ExampleStoreDialogProps) => { - const actions = React.useMemo( - () => [ - Close} - primary={false} - onClick={onClose} - />, - Create a blank project} - primary - onClick={onOpenNewProjectSetupDialog} - />, - ], - [onClose, onOpenNewProjectSetupDialog] - ); - - if (!open) return null; - - return ( - - {({ i18n }) => ( - Create a new project} - actions={actions} - onRequestClose={onClose} - onApply={onOpenNewProjectSetupDialog} - open={open} - fullHeight - flexColumnBody - > - - - )} - - ); -}; - -export default ExampleStoreDialog; diff --git a/newIDE/app/src/AssetStore/ExampleStore/ExampleThumbnailOrIcon.js b/newIDE/app/src/AssetStore/ExampleStore/ExampleThumbnailOrIcon.js index ce2bb569d76d..2df2660e8f8d 100644 --- a/newIDE/app/src/AssetStore/ExampleStore/ExampleThumbnailOrIcon.js +++ b/newIDE/app/src/AssetStore/ExampleStore/ExampleThumbnailOrIcon.js @@ -5,19 +5,20 @@ import { CorsAwareImage } from '../../UI/CorsAwareImage'; import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; import { iconWithBackgroundStyle } from '../../UI/IconContainer'; +const iconPadding = 1; + const styles = { iconBackground: { - flex: 0, display: 'flex', justifyContent: 'left', }, icon: { ...iconWithBackgroundStyle, - padding: 1, + padding: iconPadding, }, }; -const ICON_DESKTOP_HEIGHT = 120; +const ICON_DESKTOP_HEIGHT = 150; type Props = {| exampleShortHeader: ExampleShortHeader, @@ -29,7 +30,8 @@ export const ExampleThumbnailOrIcon = ({ exampleShortHeader }: Props) => { const aspectRatio = iconUrl.endsWith('square-icon.png') ? '1 / 1' : '16 / 9'; // Make the icon be full width on mobile. const height = isMobile && !isLandscape ? undefined : ICON_DESKTOP_HEIGHT; - const width = isMobile && !isLandscape ? '100%' : undefined; + const width = + isMobile && !isLandscape ? `calc(100% - ${2 * iconPadding}px)` : undefined; return (
diff --git a/newIDE/app/src/AssetStore/ExampleStore/index.js b/newIDE/app/src/AssetStore/ExampleStore/index.js index de57ec7d9d1b..c346b52c60e7 100644 --- a/newIDE/app/src/AssetStore/ExampleStore/index.js +++ b/newIDE/app/src/AssetStore/ExampleStore/index.js @@ -1,105 +1,105 @@ // @flow import * as React from 'react'; +import { type I18n as I18nType } from '@lingui/core'; import SearchBar, { type SearchBarInterface } from '../../UI/SearchBar'; -import { Column, Line } from '../../UI/Grid'; +import { Column, Line, Spacer } from '../../UI/Grid'; import { type ExampleShortHeader } from '../../Utils/GDevelopServices/Example'; import { ExampleStoreContext } from './ExampleStoreContext'; -import { ListSearchResults } from '../../UI/Search/ListSearchResults'; -import ExampleListItem from './ExampleListItem'; -import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem'; import { sendExampleDetailsOpened, sendGameTemplateInformationOpened, } from '../../Utils/Analytics/EventSender'; -import { t } from '@lingui/macro'; +import { t, Trans } from '@lingui/macro'; import { useShouldAutofocusInput } from '../../UI/Responsive/ScreenTypeMeasurer'; import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; -import PrivateGameTemplateListItem from '../PrivateGameTemplates/PrivateGameTemplateListItem'; import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext'; import { PrivateGameTemplateStoreContext } from '../PrivateGameTemplates/PrivateGameTemplateStoreContext'; +import GridList from '@material-ui/core/GridList'; +import { getExampleAndTemplateTiles } from '../../MainFrame/EditorContainers/HomePage/BuildSection/utils'; +import BackgroundText from '../../UI/BackgroundText'; +import { ColumnStackLayout } from '../../UI/Layout'; + +const styles = { + grid: { + margin: 0, + // Remove the scroll capability of the grid, the scroll view handles it. + overflow: 'unset', + }, +}; -const getItemUniqueId = ( - item: ExampleShortHeader | PrivateGameTemplateListingData -) => item.id; +// Filter out examples that aren't games. +const gameFilter = ( + item: PrivateGameTemplateListingData | ExampleShortHeader +) => { + if (item.previewImageUrls) { + // It's an example, filter out examples that are not games or have no thumbnail. + return item.tags.includes('game') && !!item.previewImageUrls[0]; + } + // It's a game template, trust it's been filtered correctly. + return true; +}; type Props = {| - isOpening: boolean, - onOpenNewProjectSetupDialog: () => void, - focusOnMount?: boolean, - selectedExampleShortHeader: ?ExampleShortHeader, - onSelectExampleShortHeader: (?ExampleShortHeader) => void, - selectedPrivateGameTemplateListingData: ?PrivateGameTemplateListingData, - onSelectPrivateGameTemplateListingData: ( - ?PrivateGameTemplateListingData - ) => void, + onSelectExampleShortHeader: ExampleShortHeader => void, + onSelectPrivateGameTemplateListingData: PrivateGameTemplateListingData => void, + i18n: I18nType, + onlyShowGames?: boolean, + columnsCount: number, + rowToInsert?: {| + row: number, + element: React.Node, + |}, |}; -export const ExampleStore = ({ - isOpening, - onOpenNewProjectSetupDialog, - focusOnMount, - // The example store is "controlled" by the parent. Useful as selected items are - // needed in MainFrame, to display them in NewProjectSetupDialog. - selectedExampleShortHeader, +const ExampleStore = ({ onSelectExampleShortHeader, - selectedPrivateGameTemplateListingData, onSelectPrivateGameTemplateListingData, + i18n, + onlyShowGames, + columnsCount, + rowToInsert, }: Props) => { const { receivedGameTemplates } = React.useContext(AuthenticatedUserContext); const { - exampleFilters, exampleShortHeadersSearchResults, - error: exampleStoreError, fetchExamplesAndFilters, - filtersState: exampleStoreFiltersState, - searchText, + searchText: exampleStoreSearchText, setSearchText: setExampleStoreSearchText, } = React.useContext(ExampleStoreContext); const { - gameTemplateFilters, - error: gameTemplateStoreError, fetchGameTemplates, exampleStore: { privateGameTemplateListingDatasSearchResults, - filtersState: gameTemplateStoreFiltersState, setSearchText: setGameTemplateStoreSearchText, }, } = React.useContext(PrivateGameTemplateStoreContext); + const [localSearchText, setLocalSearchText] = React.useState( + exampleStoreSearchText + ); const shouldAutofocusSearchbar = useShouldAutofocusInput(); const searchBarRef = React.useRef(null); React.useEffect( () => { - if (focusOnMount && shouldAutofocusSearchbar && searchBarRef.current) + if (shouldAutofocusSearchbar && searchBarRef.current) searchBarRef.current.focus(); }, - [shouldAutofocusSearchbar, focusOnMount] - ); - - // Tags are applied to both examples and game templates. - const tagsHandler = React.useMemo( - () => ({ - add: (tag: string) => { - exampleStoreFiltersState.addFilter(tag); - gameTemplateStoreFiltersState.addFilter(tag); - }, - remove: (tag: string) => { - exampleStoreFiltersState.removeFilter(tag); - gameTemplateStoreFiltersState.removeFilter(tag); - }, - // We use the same tags for both examples and game templates, so we can - // use the tags from either store. - chosenTags: exampleStoreFiltersState.chosenFilters, - }), - [exampleStoreFiltersState, gameTemplateStoreFiltersState] + [shouldAutofocusSearchbar] ); // We search in both examples and game templates stores. const setSearchText = React.useCallback( (searchText: string) => { - setExampleStoreSearchText(searchText); - setGameTemplateStoreSearchText(searchText); + if (searchText.length < 2) { + // Prevent searching with less than 2 characters, as it does not return any results. + setExampleStoreSearchText(''); + setGameTemplateStoreSearchText(''); + } else { + setExampleStoreSearchText(searchText); + setGameTemplateStoreSearchText(searchText); + } + setLocalSearchText(searchText); }, [setExampleStoreSearchText, setGameTemplateStoreSearchText] ); @@ -120,155 +120,129 @@ export const ExampleStore = ({ [fetchGameTemplatesAndExamples] ); - const getExampleShortHeaderMatches = ( - exampleShortHeader: ExampleShortHeader - ): SearchMatch[] => { - if (!exampleShortHeadersSearchResults) return []; - const exampleMatches = exampleShortHeadersSearchResults.find( - result => result.item.id === exampleShortHeader.id - ); - return exampleMatches ? exampleMatches.matches : []; - }; - - const getPrivateAssetPackListingDataMatches = ( - privateGameTemplateListingData: PrivateGameTemplateListingData - ): SearchMatch[] => { - if (!privateGameTemplateListingDatasSearchResults) return []; - const gameTemplateMatches = privateGameTemplateListingDatasSearchResults.find( - result => result.item.id === privateGameTemplateListingData.id - ); - return gameTemplateMatches ? gameTemplateMatches.matches : []; - }; - - const searchItems: ( - | ExampleShortHeader - | PrivateGameTemplateListingData - )[] = React.useMemo( + const resultTiles: React.Node[] = React.useMemo( () => { - const searchItems = []; - const privateGameTemplateItems = privateGameTemplateListingDatasSearchResults - ? privateGameTemplateListingDatasSearchResults.map(({ item }) => item) - : []; - const exampleShortHeaderItems = exampleShortHeadersSearchResults - ? exampleShortHeadersSearchResults.map(({ item }) => item) - : []; - - if (searchText || tagsHandler.chosenTags.size > 0) { - return [...privateGameTemplateItems, ...exampleShortHeaderItems]; - } - - for (let i = 0; i < exampleShortHeaderItems.length; ++i) { - searchItems.push(exampleShortHeaderItems[i]); - if (i % 2 === 1 && privateGameTemplateItems.length > 0) { - const nextPrivateGameTemplateIndex = Math.floor(i / 2); - if (nextPrivateGameTemplateIndex < privateGameTemplateItems.length) - searchItems.push( - privateGameTemplateItems[nextPrivateGameTemplateIndex] - ); - } - } - - return searchItems; + return getExampleAndTemplateTiles({ + receivedGameTemplates, + privateGameTemplateListingDatas: privateGameTemplateListingDatasSearchResults + ? privateGameTemplateListingDatasSearchResults + .map(({ item }) => item) + .filter( + privateGameTemplateListingData => + !onlyShowGames || gameFilter(privateGameTemplateListingData) + ) + : [], + exampleShortHeaders: exampleShortHeadersSearchResults + ? exampleShortHeadersSearchResults + .map(({ item }) => item) + .filter( + exampleShortHeader => + !onlyShowGames || gameFilter(exampleShortHeader) + ) + : [], + onSelectPrivateGameTemplateListingData: privateGameTemplateListingData => { + sendGameTemplateInformationOpened({ + gameTemplateName: privateGameTemplateListingData.name, + gameTemplateId: privateGameTemplateListingData.id, + source: 'examples-list', + }); + onSelectPrivateGameTemplateListingData( + privateGameTemplateListingData + ); + }, + onSelectExampleShortHeader: exampleShortHeader => { + sendExampleDetailsOpened(exampleShortHeader.slug); + onSelectExampleShortHeader(exampleShortHeader); + }, + i18n, + privateGameTemplatesPeriodicity: 1, + showOwnedGameTemplatesFirst: true, + }).allGridItems; }, [ - exampleShortHeadersSearchResults, + receivedGameTemplates, privateGameTemplateListingDatasSearchResults, - searchText, - tagsHandler, + exampleShortHeadersSearchResults, + onSelectPrivateGameTemplateListingData, + onSelectExampleShortHeader, + i18n, + onlyShowGames, ] ); - const defaultTags = React.useMemo( + const nodesToDisplay: React.Node[] = React.useMemo( () => { - const allDefaultTags = [ - ...(exampleFilters ? exampleFilters.defaultTags : []), - ...(gameTemplateFilters ? gameTemplateFilters.defaultTags : []), - ]; - const uniqueTags = new Set(allDefaultTags); - return Array.from(uniqueTags); + const numberOfTilesToDisplayUntilRowToInsert = rowToInsert + ? rowToInsert.row * columnsCount + : 0; + const firstTiles = resultTiles.slice( + 0, + numberOfTilesToDisplayUntilRowToInsert + ); + const lastTiles = resultTiles.slice( + numberOfTilesToDisplayUntilRowToInsert + ); + return [ + + {firstTiles} + , + rowToInsert ? ( + {rowToInsert.element} + ) : null, + lastTiles.length > 0 ? ( + + {lastTiles} + + ) : null, + ].filter(Boolean); }, - [exampleFilters, gameTemplateFilters] + [columnsCount, rowToInsert, resultTiles] ); return ( - + {}} - tagsHandler={tagsHandler} - tags={defaultTags} ref={searchBarRef} placeholder={t`Search examples`} /> - - { - if (item.authorIds) { - // This is an ExampleShortHeader. - return ( - { - sendExampleDetailsOpened(item.slug); - onSelectExampleShortHeader(item); - }} - onOpen={() => { - onSelectExampleShortHeader(item); - onOpenNewProjectSetupDialog(); - }} - /> - ); - } - if (item.listing) { - // This is a PrivateGameTemplateListingData. - const isTemplateOwned = - !!receivedGameTemplates && - !!receivedGameTemplates.find( - template => template.id === item.id - ); - return ( - { - onSelectPrivateGameTemplateListingData(item); - sendGameTemplateInformationOpened({ - gameTemplateName: item.name, - gameTemplateId: item.id, - source: 'examples-list', - }); - }} - owned={isTemplateOwned} - /> - ); - } - return null; // Should not happen. - }} - /> - + {resultTiles.length === 0 ? ( + + + + + No results returned for your search. Try something else! + + + {rowToInsert && {rowToInsert.element}} + + ) : ( + + {nodesToDisplay} + + )} ); }; + +export default ExampleStore; diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog.js deleted file mode 100644 index 5e6a8ce16902..000000000000 --- a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationDialog.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow -import * as React from 'react'; -import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; -import { Trans } from '@lingui/macro'; -import Dialog from '../../UI/Dialog'; -import FlatButton from '../../UI/FlatButton'; -import PrivateGameTemplateInformationPage from './PrivateGameTemplateInformationPage'; - -type Props = {| - privateGameTemplateListingData: PrivateGameTemplateListingData, - privateGameTemplateListingDatasFromSameCreator: ?Array, - onGameTemplateOpen: PrivateGameTemplateListingData => void, - onCreateWithGameTemplate: PrivateGameTemplateListingData => void, - onClose: () => void, -|}; - -const PrivateGameTemplateInformationDialog = ({ - privateGameTemplateListingData, - privateGameTemplateListingDatasFromSameCreator, - onGameTemplateOpen, - onCreateWithGameTemplate, - onClose, -}: Props) => { - return ( - Back} - primary={false} - onClick={onClose} - />, - ]} - open - onRequestClose={onClose} - minHeight="lg" - flexColumnBody - > - - - ); -}; - -export default PrivateGameTemplateInformationDialog; diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js index 07290619e838..2cf446fa3c8c 100644 --- a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js +++ b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateInformationPage.js @@ -119,7 +119,7 @@ type Props = {| privateGameTemplateListingDatasFromSameCreator?: ?Array, onGameTemplateOpen: PrivateGameTemplateListingData => void, onAssetPackOpen?: PrivateAssetPackListingData => void, - onCreateWithGameTemplate: PrivateGameTemplateListingData => void, + onCreateWithGameTemplate?: PrivateGameTemplateListingData => void, simulateAppStoreProduct?: boolean, |}; @@ -285,7 +285,7 @@ const PrivateGameTemplateInformationPage = ({ const onClickBuy = React.useCallback( async () => { if (!gameTemplate) return; - if (isAlreadyReceived) { + if (isAlreadyReceived && onCreateWithGameTemplate) { onCreateWithGameTemplate(privateGameTemplateListingData); return; } @@ -329,7 +329,7 @@ const PrivateGameTemplateInformationPage = ({ return; } - if (isAlreadyReceived) { + if (isAlreadyReceived && onCreateWithGameTemplate) { onCreateWithGameTemplate(privateGameTemplateListingData); return; } @@ -412,7 +412,7 @@ const PrivateGameTemplateInformationPage = ({ {errorText} ) : isFetching ? ( - + ) : gameTemplate && sellerPublicProfile ? ( @@ -523,17 +523,7 @@ const PrivateGameTemplateInformationPage = ({ ownedLicense={userGameTemplatePurchaseUsageType} /> - {isAlreadyReceived ? ( - - onCreateWithGameTemplate( - privateGameTemplateListingData - ) - } - label={Open template} - /> - ) : ( + {!isAlreadyReceived ? ( <> {!shouldUseOrSimulateAppStoreProduct && ( @@ -553,7 +543,17 @@ const PrivateGameTemplateInformationPage = ({ /> )} - )} + ) : onCreateWithGameTemplate ? ( + + onCreateWithGameTemplate( + privateGameTemplateListingData + ) + } + label={Open template} + /> + ) : null}
diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateListItem.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateListItem.js deleted file mode 100644 index 6d5785f8c277..000000000000 --- a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateListItem.js +++ /dev/null @@ -1,176 +0,0 @@ -// @flow -import * as React from 'react'; -import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; -import ButtonBase from '@material-ui/core/ButtonBase'; -import Text from '../../UI/Text'; -import { Trans } from '@lingui/macro'; -import { Column, Line } from '../../UI/Grid'; -import HighlightedText from '../../UI/Search/HighlightedText'; -import { type SearchMatch } from '../../UI/Search/UseSearchStructuredItem'; -import { ResponsiveLineStackLayout } from '../../UI/Layout'; -import { iconWithBackgroundStyle } from '../../UI/IconContainer'; -import Lightning from '../../UI/CustomSvgIcons/Lightning'; -import { CorsAwareImage } from '../../UI/CorsAwareImage'; -import { shouldUseAppStoreProduct } from '../../Utils/AppStorePurchases'; -import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; -import FlatButton from '../../UI/FlatButton'; -import { capitalize } from 'lodash'; -import Chip from '../../UI/Chip'; -import ProductPriceTag from '../ProductPriceTag'; - -const styles = { - container: { - display: 'flex', - overflow: 'hidden', - paddingTop: 8, - paddingBottom: 8, - paddingRight: 8, - }, - button: { - alignItems: 'flex-start', - textAlign: 'left', - flex: 1, - }, - iconBackground: { - flex: 0, - display: 'flex', - justifyContent: 'left', - }, - icon: { - ...iconWithBackgroundStyle, - padding: 1, - aspectRatio: '16 / 9', - }, - priceTagContainer: { - position: 'absolute', - top: 10, - left: 10, - cursor: 'default', - }, - chip: { - marginRight: 2, - marginBottom: 2, - }, -}; - -type Props = {| - privateGameTemplateListingData: PrivateGameTemplateListingData, - matches: ?Array, - isOpening: boolean, - onChoose: () => void, - onHeightComputed: number => void, - owned: boolean, -|}; - -const PrivateGameTemplateListItem = ({ - privateGameTemplateListingData, - matches, - isOpening, - onChoose, - onHeightComputed, - owned, -}: Props) => { - const { isMobile } = useResponsiveWindowSize(); - // Report the height of the item once it's known. - const containerRef = React.useRef(null); - React.useLayoutEffect(() => { - if (containerRef.current) - onHeightComputed(containerRef.current.getBoundingClientRect().height); - }); - - const renderGameTemplateField = (field: 'description' | 'name') => { - const originalField = privateGameTemplateListingData[field]; - - if (!matches) return originalField; - const nameMatches = matches.filter(match => match.key === field); - if (nameMatches.length === 0) return originalField; - - return ( - - ); - }; - - return ( -
- - - - {!!privateGameTemplateListingData.thumbnailUrls.length && ( - - -
- -
-
- )} - - {renderGameTemplateField('name')} - -
- {privateGameTemplateListingData.isSellerGDevelop && ( - } - variant="outlined" - color="secondary" - size="small" - style={styles.chip} - label={Ready-made} - key="premium" - /> - )} - {privateGameTemplateListingData.categories.map(category => ( - - ))} -
-
- - {renderGameTemplateField('description')} - -
-
-
- - - Open} - disabled={isOpening} - onClick={onChoose} - /> - - -
-
- ); -}; - -export default PrivateGameTemplateListItem; diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateOwnedInformationPage.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateOwnedInformationPage.js new file mode 100644 index 000000000000..9b8145f82ba9 --- /dev/null +++ b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateOwnedInformationPage.js @@ -0,0 +1,93 @@ +// @flow +import * as React from 'react'; +import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; +import Text from '../../UI/Text'; +import { Trans } from '@lingui/macro'; +import { ResponsiveLineStackLayout } from '../../UI/Layout'; +import { Column, Line } from '../../UI/Grid'; +import { MarkdownText } from '../../UI/MarkdownText'; +import { shouldUseAppStoreProduct } from '../../Utils/AppStorePurchases'; +import { OwnedProductLicense } from '../ProductLicense/ProductLicenseOptions'; +import HelpIcon from '../../UI/HelpIcon'; +import PrivateGameTemplateThumbnail from './PrivateGameTemplateThumbnail'; +import FlatButton from '../../UI/FlatButton'; +import RouterContext from '../../MainFrame/RouterContext'; + +const styles = { + openProductContainer: { + display: 'flex', + paddingLeft: 32, // To align with licensing options. + marginTop: 8, + marginBottom: 8, + }, +}; + +type Props = {| + privateGameTemplateListingData: PrivateGameTemplateListingData, + purchaseUsageType: string, + onStoreProductOpened: () => void, +|}; + +const PrivateGameTemplateOwnedInformationPage = ({ + privateGameTemplateListingData, + purchaseUsageType, + onStoreProductOpened, +}: Props) => { + const shouldUseOrSimulateAppStoreProduct = shouldUseAppStoreProduct(); + const { navigateToRoute } = React.useContext(RouterContext); + + return ( + + + + + + + + + {privateGameTemplateListingData.name} + + + + + Licensing + + + + +
+ Open in Store} + onClick={() => { + navigateToRoute('store', { + 'game-template': `product-${ + privateGameTemplateListingData.id + }`, + }); + onStoreProductOpened(); + }} + primary + /> +
+ + + +
+
+
+ ); +}; + +export default PrivateGameTemplateOwnedInformationPage; diff --git a/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateThumbnail.js b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateThumbnail.js new file mode 100644 index 000000000000..0cd1005e2840 --- /dev/null +++ b/newIDE/app/src/AssetStore/PrivateGameTemplates/PrivateGameTemplateThumbnail.js @@ -0,0 +1,59 @@ +// @flow +import * as React from 'react'; +import { type PrivateGameTemplateListingData } from '../../Utils/GDevelopServices/Shop'; +import { CorsAwareImage } from '../../UI/CorsAwareImage'; +import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; +import { iconWithBackgroundStyle } from '../../UI/IconContainer'; + +const iconPadding = 1; + +const styles = { + iconBackground: { + display: 'flex', + justifyContent: 'left', + }, + icon: { + ...iconWithBackgroundStyle, + padding: iconPadding, + aspectRatio: '16 / 9', + }, +}; + +const ICON_DESKTOP_HEIGHT = 150; + +type Props = {| + privateGameTemplateListingData: PrivateGameTemplateListingData, + simulateAppStoreProduct: boolean, +|}; + +const PrivateGameTemplateThumbnail = ({ + privateGameTemplateListingData, + simulateAppStoreProduct, +}: Props) => { + const { isMobile, isLandscape } = useResponsiveWindowSize(); + const iconUrl = React.useMemo( + () => + (simulateAppStoreProduct && + privateGameTemplateListingData.appStoreThumbnailUrls && + privateGameTemplateListingData.appStoreThumbnailUrls[0]) || + privateGameTemplateListingData.thumbnailUrls[0], + [privateGameTemplateListingData, simulateAppStoreProduct] + ); + + // Make the icon be full width on mobile. + const height = isMobile && !isLandscape ? undefined : ICON_DESKTOP_HEIGHT; + const width = + isMobile && !isLandscape ? `calc(100% - ${2 * iconPadding}px)` : undefined; + + return ( +
+ +
+ ); +}; + +export default PrivateGameTemplateThumbnail; diff --git a/newIDE/app/src/AssetStore/ShopTiles.js b/newIDE/app/src/AssetStore/ShopTiles.js index 0a17114db47d..d44da0068d64 100644 --- a/newIDE/app/src/AssetStore/ShopTiles.js +++ b/newIDE/app/src/AssetStore/ShopTiles.js @@ -17,7 +17,7 @@ import makeStyles from '@material-ui/core/styles/makeStyles'; import { shouldValidate } from '../UI/KeyboardShortcuts/InteractionKeys'; import { CorsAwareImage } from '../UI/CorsAwareImage'; import { textEllipsisStyle } from '../UI/TextEllipsis'; -import { Column, Line } from '../UI/Grid'; +import { Column, Line, Spacer } from '../UI/Grid'; import Text from '../UI/Text'; import { Trans } from '@lingui/macro'; import ProductPriceTag, { renderProductPrice } from './ProductPriceTag'; @@ -28,6 +28,8 @@ import RaisedButton from '../UI/RaisedButton'; import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; import { ResponsiveLineStackLayout } from '../UI/Layout'; import Skeleton from '@material-ui/lab/Skeleton'; +import EmptyMessage from '../UI/EmptyMessage'; +import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer'; const styles = { priceTagContainer: { @@ -110,16 +112,19 @@ const styles = { }, }; -const useStylesForGridListItem = makeStyles(theme => - createStyles({ - tile: { - transition: 'transform 0.3s ease-in-out', - '&:hover': { - transform: 'scale(1.02)', - }, - }, - }) -); +const useStylesForGridListItem = ({ disabled }: { disabled?: boolean }) => + makeStyles(theme => + createStyles({ + tile: !disabled + ? { + transition: 'transform 0.3s ease-in-out', + '&:hover': { + transform: 'scale(1.02)', + }, + } + : {}, + }) + )(); export const AssetCardTile = ({ assetShortHeader, @@ -127,25 +132,29 @@ export const AssetCardTile = ({ size, margin, hideShortDescription, + disabled, }: {| assetShortHeader: AssetShortHeader, onOpenDetails: () => void, size: number, margin?: number, hideShortDescription?: boolean, + disabled?: boolean, |}) => { - const classesForGridListItem = useStylesForGridListItem(); + const classesForGridListItem = useStylesForGridListItem({ + disabled, + }); return ( ): void => { - if (shouldValidate(event)) { + if (shouldValidate(event) && !disabled) { onOpenDetails(); } }} - onClick={onOpenDetails} + onClick={!disabled ? onOpenDetails : undefined} style={{ margin, }} @@ -164,24 +173,28 @@ export const AssetFolderTile = ({ tag, onSelect, style, + disabled, }: {| tag: string, onSelect: () => void, /** Props needed so that GridList component can adjust tile size */ style?: any, + disabled?: boolean, |}) => { - const classesForGridListItem = useStylesForGridListItem(); + const classesForGridListItem = useStylesForGridListItem({ + disabled, + }); return ( ): void => { - if (shouldValidate(event)) { + if (shouldValidate(event) && !disabled) { onSelect(); } }} style={style} - onClick={onSelect} + onClick={!disabled ? onSelect : undefined} > @@ -199,24 +212,28 @@ export const PublicAssetPackTile = ({ assetPack, onSelect, style, + disabled, }: {| assetPack: PublicAssetPack, onSelect: () => void, /** Props needed so that GridList component can adjust tile size */ style?: any, + disabled?: boolean, |}) => { - const classesForGridListItem = useStylesForGridListItem(); + const classesForGridListItem = useStylesForGridListItem({ + disabled, + }); return ( ): void => { - if (shouldValidate(event)) { + if (shouldValidate(event) && !disabled) { onSelect(); } }} style={style} - onClick={onSelect} + onClick={!disabled ? onSelect : undefined} >
void, /** Props needed so that GridList component can adjust tile size */ style?: any, owned: boolean, + disabled?: boolean, |}) => { - const classesForGridListItem = useStylesForGridListItem(); + const classesForGridListItem = useStylesForGridListItem({ + disabled, + }); return ( ): void => { - if (shouldValidate(event)) { + if (shouldValidate(event) && !disabled) { onSelect(); } }} style={style} - onClick={onSelect} + onClick={!disabled ? onSelect : undefined} >
@@ -420,6 +441,7 @@ export const CategoryTile = ({ imageAlt, onSelect, style, + disabled, }: {| id: string, title: React.Node, @@ -428,20 +450,23 @@ export const CategoryTile = ({ onSelect: () => void, /** Props needed so that GridList component can adjust tile size */ style?: any, + disabled?: boolean, |}) => { - const classesForGridListItem = useStylesForGridListItem(); + const classesForGridListItem = useStylesForGridListItem({ + disabled, + }); const gdevelopTheme = React.useContext(GDevelopThemeContext); return ( ): void => { - if (shouldValidate(event)) { + if (shouldValidate(event) && !disabled) { onSelect(); } }} style={style} - onClick={onSelect} + onClick={!disabled ? onSelect : undefined} >
void, /** Props needed so that GridList component can adjust tile size */ style?: any, owned: boolean, + disabled?: boolean, |}) => { - const classesForGridListItem = useStylesForGridListItem(); + const { isMobile } = useResponsiveWindowSize(); + const classesForGridListItem = useStylesForGridListItem({ + disabled, + }); return ( ): void => { - if (shouldValidate(event)) { + if (shouldValidate(event) && !disabled) { onSelect(); } }} style={style} - onClick={onSelect} + onClick={!disabled ? onSelect : undefined} >
+ {isMobile && } - + {privateGameTemplateListingData.name} @@ -522,15 +553,20 @@ export const ExampleTile = ({ onSelect, style, customTitle, + centerTitle, useQuickCustomizationThumbnail, + disabled, }: {| exampleShortHeader: ExampleShortHeader | null, onSelect: () => void, /** Props needed so that GridList component can adjust tile size */ style?: any, customTitle?: string, + centerTitle?: boolean, useQuickCustomizationThumbnail?: boolean, + disabled?: boolean, |}) => { + const { isMobile } = useResponsiveWindowSize(); const thumbnailImgUrl = React.useMemo( () => { if (!exampleShortHeader) return ''; @@ -546,27 +582,38 @@ export const ExampleTile = ({ [exampleShortHeader, useQuickCustomizationThumbnail] ); - const classesForGridListItem = useStylesForGridListItem(); + const classesForGridListItem = useStylesForGridListItem({ disabled }); return ( ): void => { - if (shouldValidate(event)) { + if (shouldValidate(event) && !disabled) { onSelect(); } }} style={style} - onClick={onSelect} + onClick={!disabled ? onSelect : undefined} >
{exampleShortHeader ? ( - + thumbnailImgUrl ? ( + + ) : ( + + {exampleShortHeader.name} + + ) ) : ( )} - + {isMobile && } + + )} + {limits && hasTooManyCloudProjects ? ( + + openSubscriptionDialog({ + analyticsMetadata: { + reason: 'Cloud Project limit reached', + }, + }) + } + /> + ) : null} )} - {limits && hasTooManyCloudProjects ? ( - - openSubscriptionDialog({ - analyticsMetadata: { - reason: 'Cloud Project limit reached', - }, - }) - } - /> - ) : null} - - {isGeneratingProject && generatingProjectId && ( - { - setGeneratingProjectId(null); - setIsGeneratingProject(false); - }} - /> - )} + )} diff --git a/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js b/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js index ba16bbfd3167..96e5653214c3 100644 --- a/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js +++ b/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js @@ -3,7 +3,7 @@ import * as React from 'react'; import { I18n } from '@lingui/react'; import { ExampleStoreContext } from '../AssetStore/ExampleStore/ExampleStoreContext'; import { ExampleTile } from '../AssetStore/ShopTiles'; -import { GridList } from '@material-ui/core'; +import GridList from '@material-ui/core/GridList'; import { type ExampleShortHeader } from '../Utils/GDevelopServices/Example'; import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer'; import { type QuickCustomizationRecommendation } from '../Utils/GDevelopServices/User'; diff --git a/newIDE/app/src/UI/Dialog.js b/newIDE/app/src/UI/Dialog.js index e7da6f5b488a..c559c7e8fd69 100644 --- a/newIDE/app/src/UI/Dialog.js +++ b/newIDE/app/src/UI/Dialog.js @@ -148,22 +148,24 @@ const useDangerousStylesForDialog = (dangerLevel?: 'warning' | 'danger') => // Customize scrollbar inside Dialog so that it gives a bit of space // to the content. -const useStylesForDialogContent = makeStyles({ - root: { - '&::-webkit-scrollbar': { - width: 11, - }, - '&::-webkit-scrollbar-track': { - background: 'rgba(0, 0, 0, 0.04)', - borderRadius: 6, - }, - '&::-webkit-scrollbar-thumb': { - border: '3px solid rgba(0, 0, 0, 0)', - backgroundClip: 'padding-box', - borderRadius: 6, +const useStylesForDialogContent = ({ forceScroll }: { forceScroll: boolean }) => + makeStyles({ + root: { + ...(forceScroll ? { overflowY: 'scroll' } : {}), // Force a scrollbar to prevent layout shifts. + '&::-webkit-scrollbar': { + width: 11, + }, + '&::-webkit-scrollbar-track': { + background: 'rgba(0, 0, 0, 0.04)', + borderRadius: 6, + }, + '&::-webkit-scrollbar-thumb': { + border: '3px solid rgba(0, 0, 0, 0)', + backgroundClip: 'padding-box', + borderRadius: 6, + }, }, - }, -}); + })(); // We support a subset of the props supported by Material-UI v0.x Dialog // They should be self descriptive - refer to Material UI docs otherwise. @@ -219,6 +221,8 @@ type DialogProps = {| fullHeight?: boolean, fullscreen?: 'never-even-on-mobile' | 'always-even-on-desktop', actionsFullWidthOnMobile?: boolean, + // Useful when the content of the dialog can change and we want to avoid layout shifts. + forceScrollVisible?: boolean, id?: ?string, |}; @@ -249,6 +253,7 @@ const Dialog = ({ exceptionallyStillAllowRenderingInstancesEditors, fullscreen, actionsFullWidthOnMobile, + forceScrollVisible, }: DialogProps) => { const preferences = React.useContext(PreferencesContext); const gdevelopTheme = React.useContext(GDevelopThemeContext); @@ -265,7 +270,9 @@ const Dialog = ({ : isMobile; const classesForDangerousDialog = useDangerousStylesForDialog(dangerLevel); - const classesForDialogContent = useStylesForDialogContent(); + const classesForDialogContent = useStylesForDialogContent({ + forceScroll: !!forceScrollVisible, + }); const dialogActions = React.useMemo( () => ( diff --git a/newIDE/app/src/UI/Layout.js b/newIDE/app/src/UI/Layout.js index a0c26edb99cd..8efeee6a1e8d 100644 --- a/newIDE/app/src/UI/Layout.js +++ b/newIDE/app/src/UI/Layout.js @@ -44,6 +44,12 @@ const textFieldWithButtonLayoutStyles = { marginTop: 0, // Properly align with the text field (only "standard" text fields with margin "none" supported) marginLeft: 8, }, + textFieldOnMobileMargins: { + ...buttonCommonStyles, + // Thanks to the ResponsiveLineStackLayout, the text field is full width on mobile and is spaced out already. + marginTop: 0, + marginLeft: 0, + }, }; /** @@ -55,15 +61,19 @@ export const TextFieldWithButtonLayout = ({ renderTextField, renderButton, }: TextFieldWithButtonLayoutProps) => { + const { isMobile, isLandscape } = useResponsiveWindowSize(); return ( {renderTextField()} {renderButton( - margin === 'none' + isMobile && !isLandscape + ? textFieldWithButtonLayoutStyles.textFieldOnMobileMargins + : margin === 'none' ? noFloatingLabelText ? textFieldWithButtonLayoutStyles.standardTextFieldWithoutLabelRightButtonMargins : textFieldWithButtonLayoutStyles.standardTextFieldWithLabelRightButtonMargins diff --git a/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json b/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json index 0b7c1477a34c..bd66e04cbdff 100644 --- a/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/BlueDarkTheme/theme.json @@ -75,6 +75,11 @@ "comment": "Palette/Grey/100" } }, + "hover": { + "background-color": { + "value": "rgba(255, 255, 255, 0.08)" + } + }, "surface": { "titlebar": { "background-color": { @@ -842,4 +847,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json b/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json index 8c8f79840037..5dd7cedf715e 100644 --- a/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/DefaultDarkTheme/theme.json @@ -108,6 +108,11 @@ "comment": "Palette/Grey/100" } }, + "hover": { + "background-color": { + "value": "rgba(255, 255, 255, 0.08)" + } + }, "surface": { "titlebar": { "background-color": { @@ -900,4 +905,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json b/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json index 4faf7bb3953f..ec5f2f7c470f 100644 --- a/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json +++ b/newIDE/app/src/UI/Theme/DefaultLightTheme/theme.json @@ -108,6 +108,11 @@ "comment": "Palette/Grey/100" } }, + "hover": { + "background-color": { + "value": "rgba(0, 0, 0, 0.04)" + } + }, "surface": { "titlebar": { "background-color": { @@ -899,4 +904,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/UI/Theme/NordTheme/theme.json b/newIDE/app/src/UI/Theme/NordTheme/theme.json index 63eb69a1f6eb..24245b0c267b 100644 --- a/newIDE/app/src/UI/Theme/NordTheme/theme.json +++ b/newIDE/app/src/UI/Theme/NordTheme/theme.json @@ -73,6 +73,11 @@ "comment": "Palette/Grey/100" } }, + "hover": { + "background-color": { + "value": "rgba(255, 255, 255, 0.08)" + } + }, "surface": { "titlebar": { "background-color": { @@ -832,4 +837,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json b/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json index f804da7d936e..82bfb57067c0 100644 --- a/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/OneDarkTheme/theme.json @@ -73,6 +73,11 @@ "comment": "Palette/Grey/100" } }, + "hover": { + "background-color": { + "value": "rgba(255, 255, 255, 0.08)" + } + }, "surface": { "titlebar": { "background-color": { @@ -841,4 +846,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/UI/Theme/RosePineTheme/theme.json b/newIDE/app/src/UI/Theme/RosePineTheme/theme.json index 52eb83f81661..ca1a4046c64c 100644 --- a/newIDE/app/src/UI/Theme/RosePineTheme/theme.json +++ b/newIDE/app/src/UI/Theme/RosePineTheme/theme.json @@ -73,6 +73,11 @@ "comment": "Palette/Grey/100" } }, + "hover": { + "background-color": { + "value": "rgba(255, 255, 255, 0.08)" + } + }, "surface": { "titlebar": { "background-color": { @@ -833,4 +838,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json b/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json index a9307d757298..d99c8bc8c366 100644 --- a/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json +++ b/newIDE/app/src/UI/Theme/SolarizedDarkTheme/theme.json @@ -73,6 +73,11 @@ "comment": "Palette/Grey/100" } }, + "hover": { + "background-color": { + "value": "rgba(255, 255, 255, 0.08)" + } + }, "surface": { "titlebar": { "background-color": { @@ -834,4 +839,4 @@ } } } -} \ No newline at end of file +} diff --git a/newIDE/app/src/Utils/GDevelopServices/Example.js b/newIDE/app/src/Utils/GDevelopServices/Example.js index 07ead0b7b72a..4477b5f173ca 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Example.js +++ b/newIDE/app/src/Utils/GDevelopServices/Example.js @@ -9,6 +9,7 @@ export type ExampleShortHeader = {| slug: string, name: string, shortDescription: string, + description: string, license: string, tags: Array, authors?: Array, @@ -22,7 +23,6 @@ export type ExampleShortHeader = {| export type Example = {| ...ExampleShortHeader, - description: string, projectFileUrl: string, authors: Array, |}; diff --git a/newIDE/app/src/Utils/UseCreateProject.js b/newIDE/app/src/Utils/UseCreateProject.js index ce4db1b19963..e843d6e5a4c5 100644 --- a/newIDE/app/src/Utils/UseCreateProject.js +++ b/newIDE/app/src/Utils/UseCreateProject.js @@ -8,7 +8,6 @@ import { createNewProjectFromExampleShortHeader, createNewProjectFromPrivateGameTemplate, createNewProjectFromTutorialTemplate, - createNewProjectWithDefaultLogin, type NewProjectSource, } from '../ProjectCreation/CreateProject'; import { type NewProjectSetup } from '../ProjectCreation/NewProjectSetupDialog'; @@ -379,15 +378,6 @@ const useCreateProject = ({ [beforeCreatingProject, createProject, tutorials] ); - const createProjectWithLogin = React.useCallback( - async (newProjectSetup: NewProjectSetup) => { - beforeCreatingProject(); - const newProjectSource = createNewProjectWithDefaultLogin(); - await createProject(newProjectSource, newProjectSetup); - }, - [beforeCreatingProject, createProject] - ); - const createProjectFromAIGeneration = React.useCallback( async (projectFileUrl: string, newProjectSetup: NewProjectSetup) => { beforeCreatingProject(); @@ -405,7 +395,6 @@ const useCreateProject = ({ createProjectFromPrivateGameTemplate, createProjectFromInAppTutorial, createProjectFromTutorial, - createProjectWithLogin, createProjectFromAIGeneration, }; }; diff --git a/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js b/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js index 692c93178cad..95137c2fb7f8 100644 --- a/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js +++ b/newIDE/app/src/fixtures/GDevelopServicesTestData/index.js @@ -4057,6 +4057,7 @@ export const exampleFromFutureVersion: ExampleShortHeader = { slug: 'fake-slug', name: 'Fake example', shortDescription: 'This is a fake example made in a future GDevelop version', + description: '', license: 'MIT', tags: [], previewImageUrls: [], @@ -4070,6 +4071,7 @@ export const geometryMonsterExampleShortHeader: ExampleShortHeader = { slug: 'geometry-monster', shortDescription: 'A hyper casual endless game where you have to collect shapes and avoid bombs, with a progressively increasing difficulty.\n', + description: '', license: 'MIT', previewImageUrls: [ 'https://resources.gdevelop-app.com/examples/geometry-monster/thumbnail.png', diff --git a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleDialog.stories.js b/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleDialog.stories.js deleted file mode 100644 index 8eefd094cc6c..000000000000 --- a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleDialog.stories.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow -import * as React from 'react'; -import { action } from '@storybook/addon-actions'; - -import paperDecorator from '../../../PaperDecorator'; -import { ExampleDialog } from '../../../../AssetStore/ExampleStore/ExampleDialog'; -import { exampleFromFutureVersion } from '../../../../fixtures/GDevelopServicesTestData'; - -export default { - title: 'AssetStore/ExampleStore/ExampleDialog', - component: ExampleDialog, - decorators: [paperDecorator], -}; - -export const FutureVersion = () => ( - -); diff --git a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStore.stories.js b/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStore.stories.js deleted file mode 100644 index bada9e26a5ab..000000000000 --- a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStore.stories.js +++ /dev/null @@ -1,31 +0,0 @@ -// @flow -import * as React from 'react'; -import { action } from '@storybook/addon-actions'; - -import paperDecorator from '../../../PaperDecorator'; -import { ExampleStore } from '../../../../AssetStore/ExampleStore'; -import FixedHeightFlexContainer from '../../../FixedHeightFlexContainer'; -import { ExampleStoreStateProvider } from '../../../../AssetStore/ExampleStore/ExampleStoreContext'; - -export default { - title: 'AssetStore/ExampleStore', - component: ExampleStore, - decorators: [paperDecorator], -}; - -export const Default = () => ( - - - - - -); diff --git a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStoreDialog.stories.js b/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStoreDialog.stories.js deleted file mode 100644 index 07671aea95b5..000000000000 --- a/newIDE/app/src/stories/componentStories/AssetStore/ExampleStore/ExampleStoreDialog.stories.js +++ /dev/null @@ -1,29 +0,0 @@ -// @flow -import * as React from 'react'; -import paperDecorator from '../../../PaperDecorator'; -import { action } from '@storybook/addon-actions'; -import { ExampleStoreStateProvider } from '../../../../AssetStore/ExampleStore/ExampleStoreContext'; -import ExampleStoreDialog from '../../../../AssetStore/ExampleStore/ExampleStoreDialog'; - -export default { - title: 'Project Creation/ExampleStoreDialog', - component: ExampleStoreDialog, - decorators: [paperDecorator], -}; - -export const Default = () => ( - - - -); diff --git a/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js b/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js index a8e3d5f086a5..bb034d5d7d7a 100644 --- a/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js +++ b/newIDE/app/src/stories/componentStories/HomePage/HomePage.stories.js @@ -93,12 +93,11 @@ const WrappedHomePage = ({ storageProviders={[CloudStorageProvider]} onChooseProject={() => action('onChooseProject')()} onOpenRecentFile={() => action('onOpenRecentFile')()} - onOpenExampleStore={() => action('onOpenExampleStore')()} onSelectExampleShortHeader={() => action('onSelectExampleShortHeader')() } - onPreviewPrivateGameTemplateListingData={() => - action('onPreviewPrivateGameTemplateListingData')() + onSelectPrivateGameTemplateListingData={() => + action('onSelectPrivateGameTemplateListingData')() } onOpenPrivateGameTemplateListingData={() => action('onOpenPrivateGameTemplateListingData')() diff --git a/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js b/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js index fef4525fe5da..9f9d870a2064 100644 --- a/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js +++ b/newIDE/app/src/stories/componentStories/HomePage/LearnSection.stories.js @@ -44,7 +44,6 @@ export const Default = () => ( > {}} selectInAppTutorial={action('selectInAppTutorial')} onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} @@ -66,7 +65,6 @@ export const NotAuthenticated = () => ( > {}} selectInAppTutorial={action('selectInAppTutorial')} onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} @@ -90,7 +88,6 @@ export const EducationSubscriber = () => ( > {}} selectInAppTutorial={action('selectInAppTutorial')} onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} @@ -114,7 +111,6 @@ export const EducationTeacher = () => ( > {}} selectInAppTutorial={action('selectInAppTutorial')} onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} @@ -135,7 +131,6 @@ export const Loading = () => ( > {}} selectInAppTutorial={action('selectInAppTutorial')} onOpenTemplateFromTutorial={action('onOpenTemplateFromTutorial')} diff --git a/newIDE/app/src/stories/componentStories/ProjectCreation/NewProjectSetupDialog.stories.js b/newIDE/app/src/stories/componentStories/ProjectCreation/NewProjectSetupDialog.stories.js index ba8ec853a160..e6c8af986cf6 100644 --- a/newIDE/app/src/stories/componentStories/ProjectCreation/NewProjectSetupDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/ProjectCreation/NewProjectSetupDialog.stories.js @@ -15,6 +15,7 @@ import { geometryMonsterExampleShortHeader, fakePrivateGameTemplateListingData, } from '../../../fixtures/GDevelopServicesTestData'; +import AuthenticatedUserContext from '../../../Profile/AuthenticatedUserContext'; export default { title: 'Project Creation/NewProjectSetupDialog', @@ -24,149 +25,241 @@ export default { export const OpenAndNotAuthenticated = () => { return ( - action('click on close')()} - onCreateEmptyProject={() => action('create empty')()} - onCreateFromExample={() => action('create from example')()} - onCreateWithLogin={() => action('create with login')()} - onCreateFromAIGeneration={() => action('create from AI generation')()} - onCreateProjectFromPrivateGameTemplate={() => - action('create project from private game template')() - } - selectedExampleShortHeader={null} - selectedPrivateGameTemplateListingData={null} - /> + + action('click on close')()} + onCreateEmptyProject={() => action('create empty')()} + onCreateFromExample={() => action('create from example')()} + onCreateFromAIGeneration={() => action('create from AI generation')()} + onCreateProjectFromPrivateGameTemplate={() => + action('create project from private game template')() + } + selectedExampleShortHeader={null} + selectedPrivateGameTemplateListingData={null} + onSelectExampleShortHeader={() => action('select example')()} + onSelectPrivateGameTemplateListingData={() => + action('select private game template')() + } + privateGameTemplateListingDatasFromSameCreator={[]} + /> + ); }; export const OpenAndAuthenticated = () => { return ( - action('click on close')()} - onCreateEmptyProject={() => action('create empty')()} - onCreateFromExample={() => action('create from example')()} - onCreateWithLogin={() => action('create with login')()} - onCreateFromAIGeneration={() => action('create from AI generation')()} - onCreateProjectFromPrivateGameTemplate={() => - action('create project from private game template')() - } - selectedExampleShortHeader={null} - selectedPrivateGameTemplateListingData={null} - /> + + action('click on close')()} + onCreateEmptyProject={() => action('create empty')()} + onCreateFromExample={() => action('create from example')()} + onCreateFromAIGeneration={() => action('create from AI generation')()} + onCreateProjectFromPrivateGameTemplate={() => + action('create project from private game template')() + } + selectedExampleShortHeader={null} + selectedPrivateGameTemplateListingData={null} + onSelectExampleShortHeader={() => action('select example')()} + onSelectPrivateGameTemplateListingData={() => + action('select private game template')() + } + privateGameTemplateListingDatasFromSameCreator={[]} + /> + ); }; export const Opening = () => { return ( - action('click on close')()} - onCreateEmptyProject={() => action('create empty')()} - onCreateFromExample={() => action('create from example')()} - onCreateWithLogin={() => action('create with login')()} - onCreateFromAIGeneration={() => action('create from AI generation')()} - onCreateProjectFromPrivateGameTemplate={() => - action('create project from private game template')() - } - selectedExampleShortHeader={null} - selectedPrivateGameTemplateListingData={null} - /> + + action('click on close')()} + onCreateEmptyProject={() => action('create empty')()} + onCreateFromExample={() => action('create from example')()} + onCreateFromAIGeneration={() => action('create from AI generation')()} + onCreateProjectFromPrivateGameTemplate={() => + action('create project from private game template')() + } + selectedExampleShortHeader={null} + selectedPrivateGameTemplateListingData={null} + onSelectExampleShortHeader={() => action('select example')()} + onSelectPrivateGameTemplateListingData={() => + action('select private game template')() + } + privateGameTemplateListingDatasFromSameCreator={[]} + /> + ); }; export const LimitsReached = () => { return ( - action('click on close')()} - onCreateEmptyProject={() => action('create empty')()} - onCreateFromExample={() => action('create from example')()} - onCreateWithLogin={() => action('create with login')()} - onCreateFromAIGeneration={() => action('create from AI generation')()} - onCreateProjectFromPrivateGameTemplate={() => - action('create project from private game template')() - } - selectedExampleShortHeader={null} - selectedPrivateGameTemplateListingData={null} - /> + + action('click on close')()} + onCreateEmptyProject={() => action('create empty')()} + onCreateFromExample={() => action('create from example')()} + onCreateFromAIGeneration={() => action('create from AI generation')()} + onCreateProjectFromPrivateGameTemplate={() => + action('create project from private game template')() + } + selectedExampleShortHeader={null} + selectedPrivateGameTemplateListingData={null} + onSelectExampleShortHeader={() => action('select example')()} + onSelectPrivateGameTemplateListingData={() => + action('select private game template')() + } + privateGameTemplateListingDatasFromSameCreator={[]} + /> + ); }; export const FromExample = () => { return ( - action('click on close')()} - onCreateEmptyProject={() => action('create empty')()} - onCreateFromExample={() => action('create from example')()} - onCreateWithLogin={() => action('create with login')()} - onCreateFromAIGeneration={() => action('create from AI generation')()} - selectedExampleShortHeader={geometryMonsterExampleShortHeader} - onCreateProjectFromPrivateGameTemplate={() => - action('create project from private game template')() - } - selectedPrivateGameTemplateListingData={null} - /> + + action('click on close')()} + onCreateEmptyProject={() => action('create empty')()} + onCreateFromExample={() => action('create from example')()} + onCreateFromAIGeneration={() => action('create from AI generation')()} + selectedExampleShortHeader={geometryMonsterExampleShortHeader} + onCreateProjectFromPrivateGameTemplate={() => + action('create project from private game template')() + } + selectedPrivateGameTemplateListingData={null} + onSelectExampleShortHeader={() => action('select example')()} + onSelectPrivateGameTemplateListingData={() => + action('select private game template')() + } + privateGameTemplateListingDatasFromSameCreator={[]} + /> + + ); +}; + +export const FromExampleWithoutGoingBack = () => { + return ( + + action('click on close')()} + onCreateEmptyProject={() => action('create empty')()} + onCreateFromExample={() => action('create from example')()} + onCreateFromAIGeneration={() => action('create from AI generation')()} + selectedExampleShortHeader={geometryMonsterExampleShortHeader} + onCreateProjectFromPrivateGameTemplate={() => + action('create project from private game template')() + } + selectedPrivateGameTemplateListingData={null} + onSelectExampleShortHeader={() => action('select example')()} + onSelectPrivateGameTemplateListingData={() => + action('select private game template')() + } + privateGameTemplateListingDatasFromSameCreator={[]} + preventBackHome + /> + ); }; export const FromPrivateGameTemplate = () => { return ( - action('click on close')()} - onCreateEmptyProject={() => action('create empty')()} - onCreateFromExample={() => action('create from example')()} - onCreateWithLogin={() => action('create with login')()} - onCreateFromAIGeneration={() => action('create from AI generation')()} - selectedExampleShortHeader={null} - onCreateProjectFromPrivateGameTemplate={() => - action('create project from private game template')() - } - selectedPrivateGameTemplateListingData={ - fakePrivateGameTemplateListingData - } - /> + + action('click on close')()} + onCreateEmptyProject={() => action('create empty')()} + onCreateFromExample={() => action('create from example')()} + onCreateFromAIGeneration={() => action('create from AI generation')()} + selectedExampleShortHeader={null} + onCreateProjectFromPrivateGameTemplate={() => + action('create project from private game template')() + } + selectedPrivateGameTemplateListingData={ + fakePrivateGameTemplateListingData + } + onSelectExampleShortHeader={() => action('select example')()} + onSelectPrivateGameTemplateListingData={() => + action('select private game template')() + } + privateGameTemplateListingDatasFromSameCreator={[]} + /> + + ); +}; + +export const FromPrivateGameTemplateWithoutGoingBack = () => { + return ( + + action('click on close')()} + onCreateEmptyProject={() => action('create empty')()} + onCreateFromExample={() => action('create from example')()} + onCreateFromAIGeneration={() => action('create from AI generation')()} + selectedExampleShortHeader={null} + onCreateProjectFromPrivateGameTemplate={() => + action('create project from private game template')() + } + selectedPrivateGameTemplateListingData={ + fakePrivateGameTemplateListingData + } + onSelectExampleShortHeader={() => action('select example')()} + onSelectPrivateGameTemplateListingData={() => + action('select private game template')() + } + privateGameTemplateListingDatasFromSameCreator={[]} + preventBackHome + /> + ); };