diff --git a/src/assets/images/spoke_logo.png b/src/assets/images/spoke_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..f331e87ddbc8e429207dcd2879c2670419041cac Binary files /dev/null and b/src/assets/images/spoke_logo.png differ diff --git a/src/assets/stylesheets/spoke.scss b/src/assets/stylesheets/spoke.scss index 037f6dadd90e5e68c9f28dd4085d4022ee81ec10..fa551e1fe1a834fbafba9efdb62e2653d13b0c86 100644 --- a/src/assets/stylesheets/spoke.scss +++ b/src/assets/stylesheets/spoke.scss @@ -1,6 +1,7 @@ @import 'shared'; @import 'loader'; +$spoke-action-color: #2F80ED; $breakpoint: 1280px; body { @@ -23,13 +24,12 @@ body { :local(.ui) { display: flex; flex-direction: column; - height: calc(100vh); } :local(.content) { display: flex; - height: 100%; flex-direction: column; + height: calc(100vh); } :local(.header) { @@ -52,19 +52,53 @@ body { :local(.hero-pane) { display: flex; height: 100%; + position: relative; @media(max-width: $breakpoint) { flex-direction: column; } } +:local(.spoke-logo) { + position: relative; + + img { + width: 400px; + } +} + +:local(.primary-tagline) { + position: absolute; + right: 6px; + bottom: -8px; + font-weight: bold; + font-size: 1.7em; +} + +:local(.secondary-tagline) { + font-weight: lighter; + font-size: 1.5em; + text-align: center; + margin-top: 48px; + + a { + color: white; + } +} + :local(.hero-message) { width: 600px; - background-color: red; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; @media(max-width: $breakpoint) { order: 2; width: 100%; + margin: 32px 0; + height: auto; } } @@ -78,7 +112,11 @@ body { flex: 1; @media(max-width: $breakpoint) { - height: auto; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; margin: 0px } } @@ -98,3 +136,26 @@ body { z-index: -1; } + +:local(.download-button) { + @extend %action-button; + @extend %wide-button; + background-color: $spoke-action-color; + margin-top: 64px; +} + +:local(.play-button) { + @extend %action-button; + background-color: $darker-grey; + margin: auto; + margin-top: 64px; + padding: 0px 82px; +} + +:local(.browse-versions) { + color: $grey-text; + margin-top: 12px; + display: block; + text-align: center; + width: 100%; +} diff --git a/src/assets/translations.data.json b/src/assets/translations.data.json index 6213bc174429e46a1ac7f770f64a56a907de3449..886537883c73ea6b2f7cf398387af6d4065714ae 100644 --- a/src/assets/translations.data.json +++ b/src/assets/translations.data.json @@ -87,6 +87,14 @@ "link.cancel": "cancel", "invite.enter_via": "Enter via ", "invite.and_enter_code": " with code:", - "invite.or_visit": "or visit" + "invite.or_visit": "or visit", + "spoke.primary_tagline": "make your space", + "spoke.secondary_tagline": "Create 3D social scenes for ", + "spoke.download_win": "Download for Windows", + "spoke.download_macos": "Download for Mac", + "spoke.download_linux": "Download for Linux", + "spoke.download_unavailable": "View Releases", + "spoke.browse_all_versions": "Browse All Versions", + "spoke.play_button": "Watch the Video" } } diff --git a/src/spoke.js b/src/spoke.js index 684e595628cacd02bfc38f637ed20afd0f49e1e0..1689fbd153446242e2f23c856c701587d3a3dfef 100644 --- a/src/spoke.js +++ b/src/spoke.js @@ -3,8 +3,9 @@ import React, { Component } from "react"; //import PropTypes from "prop-types"; //import classNames from "classnames"; import { playVideoWithStopOnBlur } from "./utils/video-utils.js"; -import { IntlProvider, /*FormattedMessage, */ addLocaleData } from "react-intl"; +import { IntlProvider, FormattedMessage, addLocaleData } from "react-intl"; import styles from "./assets/stylesheets/spoke.scss"; +import spokeLogo from "./assets/images/spoke_logo.png"; //const qs = new URLSearchParams(location.search); @@ -20,22 +21,90 @@ addLocaleData([...en]); class SpokeLanding extends Component { static propTypes = {}; - state = {}; + state = { downloadLinkForCurrentPlatform: {} }; constructor(props) { super(props); + this.state = { platform: "win" }; } componentDidMount() { this.loadVideo(); + this.fetchReleases(); } + tryGetJson = async request => { + const text = await request.text(); + try { + return JSON.parse(text); + } catch (e) { + console.log(`JSON error parsing response from ${request.url} "${text}"`, e); + } + }; + + getDownloadUrlForPlatform = (assets, platform) => { + return assets.find(asset => asset.name.includes(platform)).downloadUrl; + }; + + fetchReleases = async () => { + // Read-only, public access token. + const token = "de8cbfb4cc0281c7b731c891df431016c29b0ace"; + const result = await fetch("https://api.github.com/graphql", { + timeout: 5000, + method: "POST", + headers: { authorization: `bearer ${token}` }, + body: JSON.stringify({ + query: ` + { + repository(owner: "mozillareality", name: "spoke") { + releases( + orderBy: { field: CREATED_AT, direction: DESC }, + first: 5 + ) { + nodes { + isPrerelease, + isDraft, + tag { name }, + releaseAssets(last: 3) { + nodes { name, downloadUrl } + } + }, + pageInfo { endCursor, hasNextPage } + } + } + } + ` + }) + }).then(this.tryGetJson); + + if (!result || !result.data) { + this.setState({ platform: "unavailable" }); + return; + } + + const releases = result.data.repository.releases; + const release = releases.nodes.find(release => /*!release.isPrerelease && */ !release.isDraft); + + if (!release) { + this.setState({ platform: "unavailable" }); + return; + } + + this.setState({ + downloadLinkForCurrentPlatform: this.getDownloadUrlForPlatform(release.releaseAssets.nodes, this.state.platform) + }); + }; + loadVideo() { const videoEl = document.querySelector("#preview-video"); playVideoWithStopOnBlur(videoEl); } render() { + const platform = this.state.platform; + const releasesLink = "https://github.com/MozillaReality/Spoke/releases"; + const downloadLink = platform === "unavailable" ? releasesLink : this.state.downloadLinkForCurrentPlatform; + return ( <IntlProvider locale={lang} messages={messages}> <div className={styles.ui}> @@ -54,7 +123,31 @@ class SpokeLanding extends Component { </div> <div className={styles.content}> <div className={styles.heroPane}> - <div className={styles.heroMessage}>Message</div> + <div className={styles.heroMessage}> + <div className={styles.spokeLogo}> + <img src={spokeLogo} /> + <div className={styles.primaryTagline}> + <FormattedMessage id="spoke.primary_tagline" /> + </div> + </div> + <div className={styles.secondaryTagline}> + <FormattedMessage id="spoke.secondary_tagline" /> + <a href="/">Hubs</a> + </div> + <div className={styles.actionButtons}> + <a href={downloadLink} className={styles.downloadButton}> + <FormattedMessage id={"spoke.download_" + this.state.platform} /> + </a> + {platform !== "unavailable" && ( + <a href={releasesLink} className={styles.browseVersions}> + <FormattedMessage id="spoke.browse_all_versions" /> + </a> + )} + <button className={styles.playButton} onClick={() => this.setState({ playVideo: true })}> + <FormattedMessage id="spoke.play_button" /> + </button> + </div> + </div> <div className={styles.heroVideo}> <video playsInline muted loop autoPlay className={styles.previewVideo} id="preview-video"> <source