From 82290540eb9f67d3c6b5e7211cc5cdd8b85fbcf4 Mon Sep 17 00:00:00 2001
From: Paul Kaplan <pkaplan@media.mit.edu>
Date: Fri, 4 May 2018 08:50:30 -0400
Subject: [PATCH] Get backpack contents from server

---
 src/components/backpack/backpack.css | 22 ++++++++++-
 src/components/backpack/backpack.jsx | 56 +++++++++++++++++++++++-----
 src/containers/backpack.jsx          | 53 ++++++++++++++++++++++++--
 src/lib/backpack-api.js              | 19 ++++++++++
 src/playground/index.jsx             |  2 +-
 5 files changed, 136 insertions(+), 16 deletions(-)
 create mode 100644 src/lib/backpack-api.js

diff --git a/src/components/backpack/backpack.css b/src/components/backpack/backpack.css
index fc5006386..db8b9cd05 100644
--- a/src/components/backpack/backpack.css
+++ b/src/components/backpack/backpack.css
@@ -26,12 +26,30 @@
     flex-direction: row;
     align-items: center;
     border-right: 1px solid $ui-black-transparent;
-    min-height: 6rem;
+    min-height: 5.5rem;
 }
 
-.empty-message {
+/* Absolute position the inner list to allow scrolling inside flex sized container */
+.backpack-list-inner {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    overflow-x: auto;
+}
+
+.status-message {
     width: 100%;
     text-align: center;
     font-size: 0.85rem;
     color: $text-primary;
 }
+
+.backpack-item {
+    min-width: 4rem;
+    margin: 0 0.25rem;
+}
diff --git a/src/components/backpack/backpack.jsx b/src/components/backpack/backpack.jsx
index 74798b8a7..81934ae14 100644
--- a/src/components/backpack/backpack.jsx
+++ b/src/components/backpack/backpack.jsx
@@ -2,10 +2,13 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import {FormattedMessage} from 'react-intl';
 import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
-
+import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx';
 import styles from './backpack.css';
 
-const Backpack = ({expanded, onToggle}) => (
+// TODO make sprite selector item not require onClick
+const noop = () => {};
+
+const Backpack = ({contents, expanded, loading, onToggle}) => (
     <div className={styles.backpackContainer}>
         <div
             className={styles.backpackHeader}
@@ -32,25 +35,60 @@ const Backpack = ({expanded, onToggle}) => (
         </div>
         {expanded ? (
             <div className={styles.backpackList}>
-                <div className={styles.emptyMessage}>
-                    <FormattedMessage
-                        defaultMessage="Backpack is empty"
-                        description="Empty backpack message"
-                        id="gui.backpack.emptyBackpack"
-                    />
-                </div>
+                {loading ? (
+                    <div className={styles.statusMessage}>
+                        <FormattedMessage
+                            defaultMessage="Loading..."
+                            description="Loading backpack message"
+                            id="gui.backpack.loadingBackpack"
+                        />
+                    </div>
+                ) : (
+                    contents.length > 0 ? (
+                        <div className={styles.backpackListInner}>
+                            {contents.map(item => (
+                                <SpriteSelectorItem
+                                    className={styles.backpackItem}
+                                    costumeURL={item.thumbnailUrl}
+                                    details={item.name}
+                                    key={item.id}
+                                    name={item.type}
+                                    selected={false}
+                                    onClick={noop}
+                                />
+                            ))}
+                        </div>
+                    ) : (
+                        <div className={styles.statusMessage}>
+                            <FormattedMessage
+                                defaultMessage="Backpack is empty"
+                                description="Empty backpack message"
+                                id="gui.backpack.emptyBackpack"
+                            />
+                        </div>
+                    )
+                )}
             </div>
         ) : null}
     </div>
 );
 
 Backpack.propTypes = {
+    contents: PropTypes.shape({
+        id: PropTypes.string,
+        thumbnailUrl: PropTypes.string,
+        type: PropTypes.string,
+        name: PropTypes.string
+    }),
     expanded: PropTypes.bool,
+    loading: PropTypes.bool,
     onToggle: PropTypes.func
 };
 
 Backpack.defaultProps = {
+    contents: [],
     expanded: false,
+    loading: false,
     onToggle: null
 };
 
diff --git a/src/containers/backpack.jsx b/src/containers/backpack.jsx
index 3af28f9e1..949b20846 100644
--- a/src/containers/backpack.jsx
+++ b/src/containers/backpack.jsx
@@ -2,26 +2,51 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import bindAll from 'lodash.bindall';
 import BackpackComponent from '../components/backpack/backpack.jsx';
+import {getBackpackContents} from '../lib/backpack-api';
+import {connect} from 'react-redux';
 
 class Backpack extends React.Component {
     constructor (props) {
         super(props);
         bindAll(this, [
-            'handleToggle'
+            'handleToggle',
+            'refreshContents'
         ]);
         this.state = {
+            offset: 0,
+            itemsPerPage: 20,
+            loading: false,
             expanded: false,
             contents: []
         };
     }
     handleToggle () {
-        this.setState({expanded: !this.state.expanded});
+        const newState = !this.state.expanded;
+        this.setState({expanded: newState, offset: 0});
+        if (newState) {
+            this.refreshContents();
+        }
+    }
+    refreshContents () {
+        if (this.props.token && this.props.username) {
+            this.setState({loading: true});
+            getBackpackContents({
+                host: this.props.host,
+                token: this.props.token,
+                username: this.props.username,
+                offset: this.state.offset,
+                limit: this.state.itemsPerPage
+            }).then(contents => {
+                this.setState({contents, loading: false});
+            });
+        }
     }
     render () {
         return (
             <BackpackComponent
                 contents={this.state.contents}
                 expanded={this.state.expanded}
+                loading={this.state.loading}
                 onToggle={this.props.host ? this.handleToggle : null}
             />
         );
@@ -29,7 +54,27 @@ class Backpack extends React.Component {
 }
 
 Backpack.propTypes = {
-    host: PropTypes.string
+    host: PropTypes.string,
+    token: PropTypes.string,
+    username: PropTypes.string
+};
+
+const mapStateToProps = state => {
+    // Look for the session state provided by scratch-www
+    if (state.session && state.session.session) {
+        return {
+            token: state.session.session.token,
+            username: state.session.session.username
+        };
+    }
+    // Otherwise try to pull testing params out of the URL, or return nulls
+    // TODO a hack for testing the backpack
+    const tokenMatches = window.location.href.match(/[?&]token=([^&]*)&?/);
+    const usernameMatches = window.location.href.match(/[?&]username=([^&]*)&?/);
+    return {
+        token: tokenMatches ? tokenMatches[1] : null,
+        username: usernameMatches ? usernameMatches[1] : null
+    };
 };
 
-export default Backpack;
+export default connect(mapStateToProps)(Backpack);
diff --git a/src/lib/backpack-api.js b/src/lib/backpack-api.js
new file mode 100644
index 000000000..e77b6d832
--- /dev/null
+++ b/src/lib/backpack-api.js
@@ -0,0 +1,19 @@
+const getBackpackContents = ({
+    host,
+    username,
+    token,
+    limit,
+    offset
+}) => fetch(`${host}${username}?limit=${limit}&offset=${offset}`, {
+    headers: {'x-token': token}
+})
+    .then(d => d.json())
+    .then(items => items.map(item =>
+        // Add a new property for the full thumbnail url, which includes the host.
+        // TODO retreiving the images through storage would allow us to remove this.
+        Object.assign(item, {thumbnailUrl: `${host}${item.thumbnail}`})
+    ));
+
+export {
+    getBackpackContents
+};
diff --git a/src/playground/index.jsx b/src/playground/index.jsx
index 9c0879f2c..47abac046 100644
--- a/src/playground/index.jsx
+++ b/src/playground/index.jsx
@@ -25,7 +25,7 @@ GUI.setAppElement(appTarget);
 const WrappedGui = HashParserHOC(AppStateHOC(GUI));
 
 // TODO a hack for testing the backpack, allow backpack host to be set by url param
-const backpackHostMatches = window.location.href.match(/[?&]backpack_host=(.*)&?/);
+const backpackHostMatches = window.location.href.match(/[?&]backpack_host=([^&]*)&?/);
 const backpackHost = backpackHostMatches ? backpackHostMatches[1] : null;
 
 const backpackOptions = {
-- 
GitLab