Hot questions for Using Spree in json

Question:

I'm building a webshop, using ReactJS for the front-end and Spree (Ruby) for the back-end.

Spree offers an API solution to connect the front-end and the back-end with one and other.

I'm trying to display products with product images, but Spree's API is setup in a specific way that product images and products aren't in the same object.

The API response is:

 {
    (holds products)data: [],
    (Holds product images)included:[],
 }

My goal is to create an ul with the product information and product image displayed.

I've tried to map my API link which

           this.state.arrays.map((product) => 
              product.data
            )

Which responds with the data object, but I cant for example do product.data.name because it returns an undefined response

DATA RESPONSE IN THE LOG

ProductsList.js:28 PL 
[undefined]
Index.js:42 productsData 
{}
ProductsList.js:28 PL 
[Array(5)]
0: Array(5)
0: {id: "5", type: "image", attributes: {…}}
1: {id: "4", type: "image", attributes: {…}}
2: {id: "1", type: "image", attributes: {…}}
3: {id: "3", type: "image", attributes: {…}}
4: {id: "2", type: "image", attributes: {…}}
length: 5
__proto__: Array(0)
length: 1
__proto__: Array(0)

Product Index page

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from "prop-types";
import ProductsList from "./products/ProductsList";
import axios from 'axios';



const REACT_VERSION = React.version;
const include = '?include=images';
const API = 'https://stern-telecom-react-salman15.c9users.io/api/v2/storefront/products' + include;

const styles = {
  card: {
    maxWidth: 345,
  },
  media: {
    height: 140,
  },
};

class Index extends React.Component {
    constructor(props){
        super(props);
        this.state = {
            products: [],
            productsData: {},
            isLoading: false,
            error: null,
    };
  }
  componentDidMount() {
    this.setState({ isLoading: true });
    axios.get(API)
      .then(result => this.setState({
        products: result.data.data,
        productsData: result.data,
        isLoading: false,
      }))
      .catch(error => this.setState({
        error,
        isLoading: false
      }));
      // console.log(
      //   'productsData', 
      //   this.state.productsData

      //   )
  }
  render() {
    const { products, productsData,isLoading, error } = this.state;

    if (error) {
      return <p>{error.message}</p>;
    }
     if (isLoading) {
      return <p>Loading ...</p>;
    }
    return (
      <React.Fragment>
          <h1>React version: {REACT_VERSION}</h1>
          <ProductsList products={this.state.productsData}/>
      </React.Fragment>
    );
  }
}

ProductsList.propTypes = {
  greeting: PropTypes.string
};

export default Index

ProductList Page

import React from "react"
import PropTypes from "prop-types"

import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';

const url = "https://stern-telecom-react-salman15.c9users.io"

class ProductsList extends React.Component {
  constructor(props) {
    super(props);
    const { products } = this.props;
    const arrays = Object.values( {products} );
    this.state = {
      products,
      arrays
    };
  }
  render () {
    return (
      <React.Fragment>
        <ul>
          <p>Shop Products</p>
          {
          // console.log(
          //   'PL',
            // this.state.arrays.map((product) => 
            //   product.data
            // )
          // )
          this.state.arrays.map(product =>
            <li key={product.objectID}>
            <Card>
                  <CardActionArea>
                    <CardMedia
                      image= {url + ''}
                      title={product.data.attributes.name}
                    />
                    <CardContent>
                      <Typography gutterBottom variant="h5" component="h2">
                       {product.data.attributes.name}
                      </Typography>
                      <Typography component="p">
                        {product.data.attributes.description}
                      </Typography>
                    </CardContent>
                  </CardActionArea>
                  <CardActions>
                    <Button size="small" color="primary">
                     {product.data.attributes.display_price} 
                    </Button>
                    <Button size="small" color="primary">
                      add to cart
                    </Button>
                  </CardActions>
                </Card>
            </li>
            )
          }
        </ul>
      </React.Fragment>
    );
  }
}

ProductsList.propTypes = {
  greeting: PropTypes.string
};
export default ProductsList

What I expect to get as Result is product information and image


Answer:

The manipulation after you're fetching json data is wrong. The returned result is a json object with a data property which is the array you want to pass and get the products.

You either pass the products to <ProductsList> component:

const { products, images, isLoading, error } = this.state;
...
<ProductsList products={products} images={images}/>

and then use it directly:

class ProductsList extends React.Component {
  constructor(props) {
    super(props);
    const { products, images } = this.props;
    this.state = {
      products,
      images
    };
    ...
  }
  ...
}

or use props.products.data to directly get the products array inside ProductsList constructor:

class ProductsList extends React.Component {
  constructor(props) {
    super(props);
    const products = this.props.products.data;
    const images = this.props.products.included;
    ...
  }
  ...
}

there is no need of using const arrays = Object.values({ products }); because you already have an array with the products:

...
products: result.data.data,   // products is an array with products
images: result.data.included, // images is an array with all posible images
productsData: result.data,    // productsData.data is an array with products
...

Also, the product object does not contain any attribute named data:

<Typography gutterBottom variant="h5" component="h2">
  {product.data.attributes.name}
</Typography>
<Typography component="p">
  {product.data.attributes.description}
</Typography>

you have to access its' properties directly like this:

<Typography gutterBottom variant="h5" component="h2">
  {product.attributes.name}
</Typography>
<Typography component="p">
  {product.attributes.description}
</Typography>

EDIT

Here is a CodeSandbox project with your code simpified and without calling the Axios request (because it's restrticted) and having the data in a JSON file instead. You should also initialize isLoading to true, or make the Index component to not render before it has some data:

class Index extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      ...
      isLoading: true,
    }
  }
}

Here is an updated screenshot with it working:

And the simplified <ProductsList/> component:

import React from "react";

const url = "https://stern-telecom-react-salman15.c9users.io";

class ProductsList extends React.Component {
  constructor(props) {
    super(props);

    const { products, images } = this.props;
    //const arrays = Object.values( {products} );
    this.state = {
      products,
      images
      //arrays
    };
  }
  render() {
    const { products, images } = this.state;
    return (
      <React.Fragment>
        <p>Shop Products</p>
        {console.log("PL", products, images)
        // this.state.arrays.map(product =>
        //   <li key={product.objectID}>

        //   </li>
        //   )
        }
        <ul>
          {products.map(product => (
            <li key={product.key}>
              <h4>{product.attributes.name}</h4>
              <p>Description: {product.attributes.description}</p>
              <p>Price: {product.attributes.display_price} </p>
              <p>Images:</p>
              <div>
                {product.relationships.images.data.map(({ id }) => {
                  let image = images.find(image => image.id == id);
                  return image ? (
                    <img src={`${url}/${image.attributes.styles[1].url}`}/>
                  ) : null;
                })}
              </div>
            </li>
          ))}
        </ul>
      </React.Fragment>
    );
  }
}

export default ProductsList;

EDIT 2

To add images it's a very simple task. You just have to combine products array with images and display the images. Check the updated <ProductsList/> component. Of course you have to pass both products and images to <ProductsList/> (const images = productsData.included;). Check the updated CodeSandbox, <ProductsList/> component and screenshot.

EDIT 3

Regarding the images; each image has a styles property which is an array of different sizes:

"included": [
{
  "id": "5",
  "type": "image",
  "attributes": {
    "viewable_type": "Spree::Variant",
    "viewable_id": 4,
    "styles": [
      {
        "url": "...",
        "width": "48",
        "height": "48"
      },
      {
        "url": "...",
        "width": "100",
        "height": "100"
      },
      {
        "url": "...",
        "width": "240",
        "height": "240"
      },
      {
        "url": "...",
        "width": "600",
        "height": "600"
      }
    ]
  }
}
...
]

in order to map the images to each product, we have to map all the images stored in each product by using product.relationships.images.data which is an array of object with id and type properties. For each image in the product images, we search through the images array using let image = images.find(image => image.id == id) and if we find an image then we use one of the four available sizes or maybe all of the available sizes (48px, 100px, 240px, 600px); I choose image.attributes.styles[1].url, so I display the second element of the available image sizes, which is the 100px size image:

product.relationships.images.data.map(({ id }) => {
  let image = images.find(image => image.id == id);
  return image ? (
    <img src={`${url}/${image.attributes.styles[1].url}`}/>
  ) : null;
})

EDIT 4

If you need to get one image per product, then you can use a function that checks whether an image exists and then gets the image from the images array:

// Here we're using an inline function to get the product image
// You can also create a normal class function and use that instead

{product.relationships.images.data.length > 0 &&
  (() => {
    // { id } is the destructure of product.relationships.images.data[0]
    // which means it extract the property id to a stand alone variable
    const { id } = product.relationships.images.data[0];
    const image = images.find(image => image.id == id);
    return image ? (
      <img src={`${url}/${image.attributes.styles[1].url}`} />
    ) : null;
  })()
}

This is an inline function that isolates its' contents and executes immediately:

(() => { ... })()

You can read more about Destructuring assignment ({ id } = object).

Question:

I am trying to write the API methods for user to sign up on spree app. It's working properly on my local machine but not on server. here is my code of user_decorator_controller.rb

def sign_up
        @user = Spree::User.find_by_email(params[:user][:email])    
        if @user.present?
          render "spree/api/users/user_exists", :status => 401 and return
        end
        @user = Spree::User.new(user_params)
        if !@user.save
          unauthorized
          return
        end
        @user.generate_spree_api_key!
      end

and sign_up.v1.rabl is

object @user
attributes :id, :spree_api_key, :email, :firstname, :lastname, :mobile

child(:bill_address => :bill_address) do
  extends "spree/api/addresses/show"
end

child(:ship_address => :ship_address) do
  extends "spree/api/addresses/show"
end

when I CURL the server with below request

 curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -X POST -d {"user":{"email":"ml5698@gmail.com","password":"12345678", "firstname":"M", "lastname":"L", "mobile":"9999888877"}} http://localhost:3000/api/users/sign_up

It gives me above error below is extracts from web server log

Started POST "/api/users/sign_up" for 127.0.0.1 at 2015-10-15 11:23:36 +0530
  ActiveRecord::SchemaMigration Load (0.3ms)  SELECT `schema_migrations`.* FROM `schema_migrations`
Error occurred while parsing request parameters.
Contents:

{user:{email:ml5698@gmail.com,password:12345678,

JSON::ParserError - 795: unexpected token at '{user:{email:ml5698@gmail.com,password:12345678,':
  json (1.8.3) lib/json/common.rb:155:in `parse'
  activesupport (4.2.4) lib/active_support/json/decoding.rb:26:in `decode'

I am using Ruby 2.2 , rails 4, json 1.8.3 , what could be the issue, please help me resolve it.


Answer:

Your error is actually in your use of your command line curl. You use the -d switch, and pass parameters to it. The parameters are only parsed until the next space, so your parameters being passed in are

{user:{email:ml5698@gmail.com,password:12345678,

This is what you're seeing in the error message, and is obviously not well formed JSON, so you get the parsing error.

Try quoting your -d parameters like so:

curl -v -H 'Content-Type: application/json' -H 'Accept: application/json' -X POST -d '{"user":{"email":"ml5698@gmail.com","password":"12345678","firstname":"M","lastname":"L","mobile":"9999888877"}}' http://localhost:3000/api/users/sign_up

Question:

I'm trying to show the line_items of an order from Solidus into the Items table in my rails app. Previously I saved the orders from Solidus to my "Order" (in my app) table with attribute "order_number"

I need to insert the order_number into the url I'm using for the external call using httparty and loop through all the rows in order_number to show the attributes of each order in my index view. Each order, for example: http://localhost:4000/api/orders/R984044507.json?token=fd5a8b4b294303ad52fcb19a9eaf8bb7325d20a8aa5abbc7 has a structure like this:

 {
"id": 3,
"number": "R984044507",
"item_total": "59.97",
"total": "68.22",
"ship_total": "5.0",
"state": "complete",
"adjustment_total": "3.25",
"user_id": null,
"created_at": "2017-07-09T17:12:19.209Z",
"updated_at": "2017-07-09T17:14:24.657Z",
"completed_at": "2017-07-09T17:14:24.657Z",
"payment_total": "0.0",
"shipment_state": "pending",
"payment_state": "balance_due",
"email": "ignaciosm@gmail.com",
"special_instructions": null,
"channel": "spree",
"included_tax_total": "0.0",
"additional_tax_total": "3.25",
"display_included_tax_total": "$0.00",
"display_additional_tax_total": "$3.25",
"tax_total": "3.25",
"currency": "USD",
"covered_by_store_credit": false,
"display_total_applicable_store_credit": "$0.00",
"order_total_after_store_credit": "68.22",
"display_order_total_after_store_credit": "$68.22",
"total_applicable_store_credit": "0.0",
"display_total_available_store_credit": "$0.00",
"display_store_credit_remaining_after_capture": "$0.00",
"canceler_id": null,
"display_item_total": "$59.97",
"total_quantity": 3,
"display_total": "$68.22",
"display_ship_total": "$5.00",
"display_tax_total": "$3.25",
"token": "vQyNyoZEdjOux5XYOCdafg",
"checkout_steps": [],
"payment_methods": [],
"bill_address": {},
"ship_address": {},
"line_items": [
{
"id": 3,
"quantity": 1,
"price": "19.99",
"variant_id": 17,
"variant": {
"product_id": 3,
"id": 17,
"name": "Ruby on Rails Baseball Jersey",
"sku": "ROR-00008",
"price": "19.99",
"weight": "0.0",
"height": null,
"width": null,
"depth": null,
"is_master": false,
"slug": "ruby-on-rails-baseball-jersey",
"description": "Fuga quia cumque sunt dignissimos eum unde dicta doloribus. Iste sit inventore doloribus et aut eum totam nisi. Ut quas explicabo omnis eos ipsa consequuntur. Aut a quis molestiae reprehenderit suscipit expedita.",
"track_inventory": true,
"cost_price": "17.0",
"option_values": [
{
"id": 3,
"name": "Large",
"presentation": "L",
"option_type_name": "tshirt-size",
"option_type_id": 1,
"option_type_presentation": "Size"
},
{
"id": 7,
"name": "Blue",
"presentation": "Blue",
"option_type_name": "tshirt-color",
"option_type_id": 2,
"option_type_presentation": "Color"
}
],
"images": [
{
"id": 15,
"position": 1,
"attachment_content_type": "image/png",
"attachment_file_name": "ror_baseball_jersey_blue.png",
"type": "Spree::Image",
"attachment_updated_at": "2017-07-08T23:10:54.595Z",
"attachment_width": 240,
"attachment_height": 240,
"alt": null,
"viewable_type": "Spree::Variant",
"viewable_id": 17,
"mini_url": "/spree/products/15/mini/ror_baseball_jersey_blue.png?1499555454",
"small_url": "/spree/products/15/small/ror_baseball_jersey_blue.png?1499555454",
"product_url": "/spree/products/15/product/ror_baseball_jersey_blue.png?1499555454",
"large_url": "/spree/products/15/large/ror_baseball_jersey_blue.png?1499555454"
},
{
"id": 16,
"position": 2,
"attachment_content_type": "image/png",
"attachment_file_name": "ror_baseball_jersey_back_blue.png",
"type": "Spree::Image",
"attachment_updated_at": "2017-07-08T23:10:55.123Z",
"attachment_width": 240,
"attachment_height": 240,
"alt": null,
"viewable_type": "Spree::Variant",
"viewable_id": 17,
"mini_url": "/spree/products/16/mini/ror_baseball_jersey_back_blue.png?1499555455",
"small_url": "/spree/products/16/small/ror_baseball_jersey_back_blue.png?1499555455",
"product_url": "/spree/products/16/product/ror_baseball_jersey_back_blue.png?1499555455",
"large_url": "/spree/products/16/large/ror_baseball_jersey_back_blue.png?1499555455"
}
],
"display_price": "$19.99",
"options_text": "Size: L, Color: Blue",
"in_stock": true,
"is_backorderable": true,
"total_on_hand": 9,
"is_destroyed": false
},
"adjustments": [
{
"id": 5,
"source_type": "Spree::TaxRate",
"source_id": 1,
"adjustable_type": "Spree::LineItem",
"adjustable_id": 3,
"amount": "1.0",
"label": "North America 5.000%",
"promotion_code": null,
"eligible": true,
"created_at": "2017-07-09T17:13:56.297Z",
"updated_at": "2017-07-09T17:14:24.280Z",
"display_amount": "$1.00"
}
],
"single_display_amount": "$19.99",
"display_amount": "$19.99",
"total": "20.99"
},
{
"id": 4,
"quantity": 1,
"price": "19.99",
"variant_id": 6,
"variant": {
"product_id": 6,
"id": 6,
"name": "Ruby Baseball Jersey",
"sku": "RUB-00001",
"price": "19.99",
"weight": "0.0",
"height": null,
"width": null,
"depth": null,
"is_master": true,
"slug": "ruby-baseball-jersey",
"description": "Fuga quia cumque sunt dignissimos eum unde dicta doloribus. Iste sit inventore doloribus et aut eum totam nisi. Ut quas explicabo omnis eos ipsa consequuntur. Aut a quis molestiae reprehenderit suscipit expedita.",
"track_inventory": true,
"cost_price": "17.0",
"option_values": [],
"images": [
{
"id": 34,
"position": 1,
"attachment_content_type": "image/png",
"attachment_file_name": "ruby_baseball.png",
"type": "Spree::Image",
"attachment_updated_at": "2017-07-08T23:11:04.206Z",
"attachment_width": 495,
"attachment_height": 477,
"alt": null,
"viewable_type": "Spree::Variant",
"viewable_id": 6,
"mini_url": "/spree/products/34/mini/ruby_baseball.png?1499555464",
"small_url": "/spree/products/34/small/ruby_baseball.png?1499555464",
"product_url": "/spree/products/34/product/ruby_baseball.png?1499555464",
"large_url": "/spree/products/34/large/ruby_baseball.png?1499555464"
}
],
"display_price": "$19.99",
"options_text": "",
"in_stock": true,
"is_backorderable": true,
"total_on_hand": 9,
"is_destroyed": false
},
"adjustments": [
{
"id": 6,
"source_type": "Spree::TaxRate",
"source_id": 1,
"adjustable_type": "Spree::LineItem",
"adjustable_id": 4,
"amount": "1.0",
"label": "North America 5.000%",
"promotion_code": null,
"eligible": true,
"created_at": "2017-07-09T17:13:56.327Z",
"updated_at": "2017-07-09T17:14:24.300Z",
"display_amount": "$1.00"
}
],
"single_display_amount": "$19.99",
"display_amount": "$19.99",
"total": "20.99"
},
{
"id": 5,
"quantity": 1,
"price": "19.99",
"variant_id": 7,
"variant": {
"product_id": 7,
"id": 7,
"name": "Apache Baseball Jersey",
"sku": "APC-00001",
"price": "19.99",
"weight": "0.0",
"height": null,
"width": null,
"depth": null,
"is_master": true,
"slug": "apache-baseball-jersey",
"description": "Fuga quia cumque sunt dignissimos eum unde dicta doloribus. Iste sit inventore doloribus et aut eum totam nisi. Ut quas explicabo omnis eos ipsa consequuntur. Aut a quis molestiae reprehenderit suscipit expedita.",
"track_inventory": true,
"cost_price": "17.0",
"option_values": [],
"images": [
{
"id": 33,
"position": 1,
"attachment_content_type": "image/png",
"attachment_file_name": "apache_baseball.png",
"type": "Spree::Image",
"attachment_updated_at": "2017-07-08T23:11:03.616Z",
"attachment_width": 504,
"attachment_height": 484,
"alt": null,
"viewable_type": "Spree::Variant",
"viewable_id": 7,
"mini_url": "/spree/products/33/mini/apache_baseball.png?1499555463",
"small_url": "/spree/products/33/small/apache_baseball.png?1499555463",
"product_url": "/spree/products/33/product/apache_baseball.png?1499555463",
"large_url": "/spree/products/33/large/apache_baseball.png?1499555463"
}
],
"display_price": "$19.99",
"options_text": "",
"in_stock": true,
"is_backorderable": true,
"total_on_hand": 9,
"is_destroyed": false
},
"adjustments": [
{
"id": 7,
"source_type": "Spree::TaxRate",
"source_id": 1,
"adjustable_type": "Spree::LineItem",
"adjustable_id": 5,
"amount": "1.0",
"label": "North America 5.000%",
"promotion_code": null,
"eligible": true,
"created_at": "2017-07-09T17:13:56.337Z",
"updated_at": "2017-07-09T17:14:24.313Z",
"display_amount": "$1.00"
}
],
"single_display_amount": "$19.99",
"display_amount": "$19.99",
"total": "20.99"
}
]
}

here is my controller:

# items controller      
     def index
          order_number = 'R984044507' # just to test until I figure out how to loop through order_ids
          url = 'http://localhost:4000/api/orders/'
          key = '.json?token=fd5a8b4b294303ad52fcb19a9eaf8bb7325d20a8aa5abbc7'
          response = HTTParty.get(url+order_number+key)

          @items = response.parsed_response    
      end

My Index looks like this:

<table>
  <thead>
    <tr>
      <th>Order</th>
      <th>Variand_Id</th>
      <th>Product</th>
      <th>Price</th>
      <th>Qty</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>

    <% (@items['line_items']).each do |item| %>
      <tr>
        <td><%= @items['number'] %></td>
        <td><%= item['variant']["id"] %></td>
        <td><%= item['variant']["name"] %></td>
        <td><%= item['variant']['price'] %></td>
        <td><%= item["quantity"] %></td>

      </tr>
    <% end %>
  </tbody>

</table>

I hard coded the order number of one of the orders in my Orders table to test that it shows the items in each order, but I don't know how to loop through the orders so I get the items from every order.

I'm not sure my approach is the best, I'm a beginner. Any help will be appreciated.


Answer:

You could do something like this:

Controller
def index
  orders = Order.all.pluck(:number)

  @items = []
  orders.each do |order|
    order_number = order
    url = 'http://localhost:4000/api/orders/'
    key = '.json?token=fd5a8b4b294303ad52fcb19a9eaf8bb7325d20a8aa5abbc7'
    response = HTTParty.get(url+order_number+key)

    @items << response.parsed_response
  end
end

First you get all order numbers (i assume that number is the column name in orders table) and then you loop through them, requesting each order and adding its attributes to @items array.

View
<table>
  <thead>
    <tr>
      <th>Order</th>
      <th>Variand_Id</th>
      <th>Product</th>
      <th>Price</th>
      <th>Qty</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @items.each do |item| %>
      <% item['line_items'].each do |line_item| %>
        <tr>
          <td><%= item['number'] %></td>
          <td><%= line_item['variant']["id"] %></td>
          <td><%= line_item['variant']["name"] %></td>
          <td><%= line_item['variant']['price'] %></td>
          <td><%= line_item["quantity"] %></td>
        </tr>
      <% end %>
    <% end %>
  </tbody>
</table>

Since @items is an array you will need to iterate it too, so two loops are needed: one for @items and one for line_items.

Although this solution should work, note that requesting a high number of orders could cause performance issues.