diff --git a/src/components/extension-landing/extension-landing.scss b/src/components/extension-landing/extension-landing.scss
index f4f4fbfcf..a9182d592 100644
--- a/src/components/extension-landing/extension-landing.scss
+++ b/src/components/extension-landing/extension-landing.scss
@@ -258,7 +258,47 @@
}
}
- div.cards + div.faq {
+ .hardware-cards {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 3em;
+
+ // 1 column
+ @media #{$medium-and-smaller} {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ .hardware-card {
+ border: 1px solid $ui-border;
+ border-radius: .5rem;
+ background-color: $ui-white;
+ overflow: hidden;
+ flex-basis: 0;
+ flex-grow: 1;
+ }
+
+ .hardware-card-image {
+ background-color: $ui-blue-10percent;
+ padding: 1rem 0 1rem;
+
+ img {
+ display: block;
+ margin: 0 auto;
+ height: 100px;
+ max-width: 100%;
+ }
+ }
+
+ .hardware-card-info {
+ padding: 1rem;
+
+ p {
+ margin: .2rem 0;
+ }
+ }
+
+ div.cards+div.faq {
padding-top: 2rem;
}
diff --git a/src/components/extension-landing/extension-requirements.jsx b/src/components/extension-landing/extension-requirements.jsx
index efe498921..ce9281396 100644
--- a/src/components/extension-landing/extension-requirements.jsx
+++ b/src/components/extension-landing/extension-requirements.jsx
@@ -12,62 +12,77 @@ const ExtensionRequirements = props => (
- {props.bluetoothStandard ? (
-
-
-
- Windows 10 version 1709+
-
-
-
- macOS 10.13+
-
-
-
-
- ChromeOS
-
-
-
- Android 6.0+
-
-
-
-
- Bluetooth
-
-
-
- Scratch Link
-
-
- ) : props.children}
+ {!props.hideWindows && (
+
+
+ Windows 10 version 1709+
+
+ )}
+ {!props.hideMac && (
+
+
+ macOS 10.13+
+
+ )}
+ {!props.hideChromeOS && (
+
+
+ ChromeOS
+
+ )}
+ {!props.hideAndroid && (
+
+
+ Android 6.0+
+
+ )}
+ {!props.hideBluetooth && (
+
+
+ Bluetooth
+
+ )}
+ {!props.hideScratchLink && (
+
+
+ Scratch Link
+
+ )}
);
ExtensionRequirements.propTypes = {
- bluetoothStandard: PropTypes.bool,
- children: PropTypes.node
+ hideAndroid: PropTypes.bool,
+ hideBluetooth: PropTypes.bool,
+ hideChromeOS: PropTypes.bool,
+ hideMac: PropTypes.bool,
+ hideScratchLink: PropTypes.bool,
+ hideWindows: PropTypes.bool
};
ExtensionRequirements.defaultProps = {
- bluetoothStandard: false
+ hideAndroid: false,
+ hideBluetooth: false,
+ hideChromeOS: false,
+ hideMac: false,
+ hideScratchLink: false,
+ hideWindows: false
};
module.exports = ExtensionRequirements;
diff --git a/src/components/extension-landing/hardware-card.jsx b/src/components/extension-landing/hardware-card.jsx
new file mode 100644
index 000000000..d3f250e1a
--- /dev/null
+++ b/src/components/extension-landing/hardware-card.jsx
@@ -0,0 +1,32 @@
+const PropTypes = require('prop-types');
+const React = require('react');
+
+const HardwareCard = props => (
+
+
+
![{props.imageAlt}]({props.imageSrc})
+
+
+
{props.title}
+
{props.description}
+
+
+);
+
+HardwareCard.propTypes = {
+ cardUrl: PropTypes.string,
+ description: PropTypes.string,
+ imageAlt: PropTypes.string,
+ imageSrc: PropTypes.string,
+ title: PropTypes.string
+};
+
+module.exports = HardwareCard;
diff --git a/src/components/os-chooser/os-chooser.jsx b/src/components/os-chooser/os-chooser.jsx
index c2821080a..232a363c8 100644
--- a/src/components/os-chooser/os-chooser.jsx
+++ b/src/components/os-chooser/os-chooser.jsx
@@ -15,49 +15,68 @@ const OSChooser = props => (
-
-
-
-
+ {!props.hideWindows && (
+
+ )}
+ {!props.hideMac && (
+
+ )}
+ {!props.hideChromeOS && (
+
+ )}
+ {!props.hideAndroid && (
+
+ )}
);
OSChooser.propTypes = {
currentOS: PropTypes.string,
- handleSetOS: PropTypes.func
+ handleSetOS: PropTypes.func,
+ hideAndroid: PropTypes.bool,
+ hideChromeOS: PropTypes.bool,
+ hideMac: PropTypes.bool,
+ hideWindows: PropTypes.bool
+};
+
+OSChooser.defaultProps = {
+ hideAndroid: false,
+ hideChromeOS: false,
+ hideMac: false,
+ hideWindows: false
};
const wrappedOSChooser = injectIntl(OSChooser);
diff --git a/src/routes.json b/src/routes.json
index 560430aaf..a780f20cb 100644
--- a/src/routes.json
+++ b/src/routes.json
@@ -298,6 +298,19 @@
"view": "download/scratch2/download",
"title": "Scratch 2.0"
},
+ {
+ "name": "download-scratch-link",
+ "pattern": "^/download/scratch-link/?(\\?.*)?$",
+ "routeAlias": "/download/scratch-link",
+ "view": "download/scratch-link/download",
+ "title": "Scratch Link Download"
+ },
+ {
+ "name": "download-scratch-link-redirect",
+ "pattern": "^/download/link/?(\\?.*)?$",
+ "routeAlias": "/download/link",
+ "redirect": "/download/scratch-link"
+ },
{
"name": "search",
"pattern": "^/search/:projects/?$",
diff --git a/src/views/boost/boost.jsx b/src/views/boost/boost.jsx
index b96565a4d..87c70c0e0 100644
--- a/src/views/boost/boost.jsx
+++ b/src/views/boost/boost.jsx
@@ -59,7 +59,7 @@ class Boost extends ExtensionLanding {
src="/images/boost/boost-header.svg"
/>}
renderRequirements={
-
+
}
/>
{
+ const [os, setOS] = useState(detectOS());
+
+ return (
+
+
+
+
+
+
{intl.formatMessage({id: 'scratchLink.headerTitle'})}
+
+
+
+
+
+
+
![](/images/download/scratch-link-illustration.svg)
+
+
+
+
+ {(isDownloaded(os)) && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {isDownloaded(os) && (
+
+
+
+
+
+
+ ),
+ macOSVersionLink: (
+
+
+
+ )
+ }}
+ />
+
+
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+ScratchLink.propTypes = {
+ intl: intlShape.isRequired
+};
+
+const WrappedScratchLink = injectIntl(ScratchLink);
+
+render(, document.getElementById('app'));
diff --git a/src/views/download/scratch-link/download.scss b/src/views/download/scratch-link/download.scss
new file mode 100644
index 000000000..1f87f12b8
--- /dev/null
+++ b/src/views/download/scratch-link/download.scss
@@ -0,0 +1,25 @@
+@import "../../../colors";
+
+.link {
+ .extension-header {
+ background-color: $ui-aqua;
+
+ .inner {
+ flex-direction: row;
+
+ .extension-info {
+ flex-direction: column;
+ align-items: flex-start;
+ margin-bottom: 0;
+
+ .extension-copy {
+ margin-bottom: 3em;
+ }
+ }
+
+ .extension-image {
+ margin: auto;
+ }
+ }
+ }
+}
diff --git a/src/views/download/scratch-link/l10n.json b/src/views/download/scratch-link/l10n.json
new file mode 100644
index 000000000..0d2ac3b18
--- /dev/null
+++ b/src/views/download/scratch-link/l10n.json
@@ -0,0 +1,24 @@
+{
+ "scratchLink.headerText": "Scratch Link allows you to connect hardware to interact with your Scratch projects. Open new possibilities by combining your digital projects with the physical world.",
+ "scratchLink.headerTitle": "Scratch Link",
+ "scratchLink.linkLogo": "Scratch Link logo",
+ "scratchLink.troubleshootingTitle": "Troubleshooting",
+ "scratchLink.checkOSVersionTitle": "Make sure your operating system is compatible with Scratch Link",
+ "scratchLink.checkOSVersionText": "The minimum operating system versions are listed at the top of this page. See instructions for checking your version of {winOSVersionLink} or {macOSVersionLink}.",
+ "scratchLink.winOSVersionLinkText": "Windows",
+ "scratchLink.macOSVersionLinkText": "Mac OS",
+ "scratchLink.closeScratchCopiesTitle": "Close other copies of Scratch",
+ "scratchLink.closeScratchCopiesText": "Only one copy of Scratch can connect with Scratch Link at a time. If you have Scratch open in other browser tabs, close it and try again.",
+ "scratchLink.thingsToTry": "Things to Try",
+ "scratchLink.compatibleDevices": "Compatible with Scratch Link",
+ "scratchLink.microbitTitle": "micro:bit",
+ "scratchLink.microbitDescription": "micro:bit is a tiny circuit board designed to help kids learn to code and create with technology.",
+ "scratchLink.ev3Title": "LEGO MINDSTORMS EV3",
+ "scratchLink.ev3Description": "LEGO MINDSTORMS Education EV3 is an invention kit with motors and sensors you can use to build interactive robotic creations.",
+ "scratchLink.wedoTitle": "LEGO Education WeDo 2.0",
+ "scratchLink.wedoDescription": "LEGO Education WeDo 2.0 is an introductory invention kit you can use to build interactive robots and other creations.",
+ "scratchLink.boostTitle": "LEGO BOOST",
+ "scratchLink.boostDescription": "The LEGO BOOST kit brings your LEGO creations to life with powerful motors, a color sensor and more.",
+ "scratchLink.vernierTitle": "Vernier Force & Acceleration",
+ "scratchLink.vernierDescription": "The Vernier Go Direct Force & Acceleration sensor is a powerful scientific tool that unlocks new ways to connect the physical world to your Scratch projects."
+}
diff --git a/src/views/ev3/ev3.jsx b/src/views/ev3/ev3.jsx
index 1ccb98b25..8c08a30c4 100644
--- a/src/views/ev3/ev3.jsx
+++ b/src/views/ev3/ev3.jsx
@@ -63,7 +63,7 @@ class EV3 extends ExtensionLanding {
videoId="0huu6wfiki"
/>}
renderRequirements={
-
+
}
/>
}
renderRequirements={
-
+
}
/>
}
renderRequirements={
-
+
}
/>
}
renderRequirements={
-
+
}
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/static/images/scratchlink/scratch-link-logo.svg b/static/images/scratchlink/scratch-link-logo.svg
new file mode 100644
index 000000000..eb5c98b86
--- /dev/null
+++ b/static/images/scratchlink/scratch-link-logo.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/test/unit/components/extension-requirements.test.jsx b/test/unit/components/extension-requirements.test.jsx
new file mode 100644
index 000000000..22b566c0e
--- /dev/null
+++ b/test/unit/components/extension-requirements.test.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
+import ExtensionRequirements from '../../../src/components/extension-landing/extension-requirements';
+
+describe('ExtensionRequirements', () => {
+
+ test('shows default extension requirements', () => {
+ const component = mountWithIntl();
+
+ const requirements = component.find('.extension-requirements span').map(span => span.text());
+
+ expect(requirements).toEqual(
+ ['Windows 10 version 1709+', 'macOS 10.13+', 'ChromeOS', 'Android 6.0+', 'Bluetooth', 'Scratch Link']
+ );
+ });
+
+ test('hides requirements', () => {
+ const component = mountWithIntl();
+
+ expect(component.find('.extension-requirements span').length).toEqual(0);
+ });
+});
diff --git a/test/unit/components/os-chooser.test.jsx b/test/unit/components/os-chooser.test.jsx
new file mode 100644
index 000000000..7658eb876
--- /dev/null
+++ b/test/unit/components/os-chooser.test.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
+import OSChooser from '../../../src/components/os-chooser/os-chooser';
+
+describe('OSChooser', () => {
+ test('calls callback when OS is selected', () => {
+ const onSetOs = jest.fn();
+ const component = mountWithIntl();
+
+ component.find('button').last()
+ .simulate('click');
+
+ expect(onSetOs).toBeCalledWith('Android');
+ });
+
+ test('has all 4 operating systems', () => {
+ const component = mountWithIntl();
+
+ expect(component.find('button').length).toEqual(4);
+ });
+
+ test('hides operating systems', () => {
+ const component = mountWithIntl();
+
+ expect(component.find('button').length).toEqual(0);
+ });
+});