diff --git a/package.json b/package.json index fee83bbe..d1a30523 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "react-helmet": "^5.2.0", "react-infinite-scroller": "^1.2.4", "react-modal": "^3.10.1", - "react-router-dom": "^5.0.1", + "react-router-dom": "^5.1.2", "react-select": "^3.0.4", "react-toggle": "^4.0.2", "react-tooltip": "^3.11.1", @@ -101,6 +101,7 @@ "three": "https://github.com/MozillaReality/three.js.git#0f9b0024725f0dd917caa54c2934a4ba1fc12c4f", "three-mesh-bvh": "^0.1.4", "url-toolkit": "^2.1.6", + "use-http": "^0.2.4", "uuid": "^3.3.3" }, "devDependencies": { diff --git a/src/api/Api.js b/src/api/Api.js index 10983024..6a928b65 100644 --- a/src/api/Api.js +++ b/src/api/Api.js @@ -94,6 +94,7 @@ export default class Project extends EventEmitter { const { protocol, host } = new URL(window.location.href); this.serverURL = protocol + "//" + host; + this.apiURL = `https://${RETICULUM_SERVER}`; this.projectDirectoryPath = "/api/files/"; @@ -357,8 +358,20 @@ export default class Project extends EventEmitter { const resp = await this.fetch(url, { headers, signal }); + if (signal.aborted) { + const error = new Error("Media search aborted"); + error.aborted = true; + throw error; + } + const json = await resp.json(); + if (signal.aborted) { + const error = new Error("Media search aborted"); + error.aborted = true; + throw error; + } + const thumbnailedEntries = json.entries.map(entry => { if (entry.images && entry.images.preview && entry.images.preview.url) { if (entry.images.preview.type === "mp4") { diff --git a/src/ui/App.js b/src/ui/App.js index 121bc26c..ca01d68a 100644 --- a/src/ui/App.js +++ b/src/ui/App.js @@ -18,7 +18,7 @@ import WhatsNewPage from "./whats-new/WhatsNewPage"; import LoginPage from "./auth/LoginPage"; import LogoutPage from "./auth/LogoutPage"; import ProjectsPage from "./projects/ProjectsPage"; -import TemplatesPage from "./projects/TemplatesPage"; +import CreateProjectPage from "./projects/CreateProjectPage"; import { ThemeProvider } from "styled-components"; @@ -76,7 +76,8 @@ export default class App extends Component { - + + diff --git a/src/ui/assets/useAssetSearch.js b/src/ui/assets/useAssetSearch.js index 860c8b4a..c72c7a86 100644 --- a/src/ui/assets/useAssetSearch.js +++ b/src/ui/assets/useAssetSearch.js @@ -14,7 +14,7 @@ function useIsMounted() { return () => ref.current; } -function useLoadAsync(callback, initialResults = [], initialCursor = 0) { +export function useLoadAsync(callback, initialResults = [], initialCursor = 0) { const currentPromise = useRef(); const abortControllerRef = useRef(); const getIsMounted = useIsMounted(); diff --git a/src/ui/contexts/ApiContext.js b/src/ui/contexts/ApiContext.js index fe559cd5..bbe4d681 100644 --- a/src/ui/contexts/ApiContext.js +++ b/src/ui/contexts/ApiContext.js @@ -1,6 +1,6 @@ import React from "react"; -const ApiContext = React.createContext(); +export const ApiContext = React.createContext(); export const ApiContextProvider = ApiContext.Provider; diff --git a/src/ui/projects/CreateProjectPage.js b/src/ui/projects/CreateProjectPage.js new file mode 100644 index 00000000..d35dca19 --- /dev/null +++ b/src/ui/projects/CreateProjectPage.js @@ -0,0 +1,164 @@ +import React, { useCallback, useState, useContext } from "react"; +import PropTypes from "prop-types"; +import ScrollToTop from "../router/ScrollToTop"; +import NavBar from "../navigation/NavBar"; +import { + ProjectGrid, + ProjectGridContainer, + ProjectGridHeader, + ProjectGridHeaderRow, + Filter, + Separator, + SearchInput, + ProjectGridContent, + ErrorMessage +} from "./ProjectGrid"; +import Footer from "../navigation/Footer"; +import PrimaryLink from "../inputs/PrimaryLink"; +import { Button } from "../inputs/Button"; +import { ProjectsSection, ProjectsContainer, ProjectsHeader } from "./ProjectsPage"; +import { ApiContext } from "../contexts/ApiContext"; +import { Link } from "react-router-dom"; +import InfiniteScroll from "react-infinite-scroller"; +import usePaginatedSearch from "./usePaginatedSearch"; + +export default function CreateProjectPage({ history, location }) { + const api = useContext(ApiContext); + + const queryParams = new URLSearchParams(location.search); + + const [params, setParams] = useState({ + source: "scene_listings", + filter: queryParams.get("filter") || "featured-remixable", + q: queryParams.get("q") || "" + }); + + const updateParams = useCallback( + nextParams => { + const search = new URLSearchParams(); + + for (const name in nextParams) { + if (name === "source" || !nextParams[name]) { + continue; + } + + search.set(name, nextParams[name]); + } + + history.push(`/projects/create?${search}`); + + setParams(nextParams); + }, + [history] + ); + + const onChangeQuery = useCallback( + value => { + updateParams({ + source: "scene_listings", + filter: "remixable", + q: value + }); + }, + [updateParams] + ); + + const onSetFeaturedRemixable = useCallback(() => { + updateParams({ + ...params, + filter: "featured-remixable", + q: "" + }); + }, [updateParams, params]); + + const onSetAll = useCallback(() => { + updateParams({ + ...params, + filter: "remixable", + q: "" + }); + }, [updateParams, params]); + + const onSelectScene = useCallback( + scene => { + const search = new URLSearchParams(); + search.set("sceneId", scene.id); + history.push(`/projects/new?${search}`); + }, + [history] + ); + + const { loading, error, entries, hasMore, loadMore } = usePaginatedSearch( + `${api.apiURL}/api/v1/media/search`, + params + ); + + const filteredEntries = entries.map(result => ({ + ...result, + url: `/projects/new?sceneId=${result.id}`, + thumbnail_url: result && result.images && result.images.preview && result.images.preview.url + })); + + return ( + <> + +
+ + + +

New Project

+ Back to projects +
+ + + + + Featured + + + All + + + + + + + + + + + {error && {error.message}} + {!error && ( + + + + )} + + +
+
+
+