Components for rendering a TeePublic storefront.
npm install teepublic-react
or
yarn add teepublic-react
Add TeePublic gem to the Gemfile, which is a wrapper around TeePublic APIs (https://api.teepublic.com/v1/docs#/)
gem 'teepublic-api', github: 'bustedtees/teepublic-api'
and run
$bundle install
Add config/initializers/tee_public.rb
and copy paste the content below with your api key
require 'tee_public/api'
TeePublic::Api.configure do |config|
config.api_key = 'YOUR_API_KEY_GOES_HERE'
end
Open rails console rails c
and try these
fetch_stores = TeePublic::Api.send('stores')
stores = fetch_stores.dig('body', '_embedded', 'stores').map(&:deep_symbolize_keys!)
puts stores
# store_id -> Pick a store_id returned by List of Stores API
# page -> For trial purposes, you can use page = 1
store_url = "stores/#{store_id}?page=#{page}"
# album_id -> it's an additional filter option, can keep it blank for now,
# we will learn more about this later on
store_url += "&album_id=#{album_id}" if album_id.present?
# product_type -> same explanation as album_id
store_url += "&product_type=#{product_type}" if product_type.present?
fetch_store = TeePublic::Api.send(store_url).deep_symbolize_keys!
store = fetch_store[:body]
puts store
If no designs are found in Single Store API that means store has no designs added on teepublic.com. Head to TeePublic.com, locate that store from slug, '/stores/'. Add few designs to it.
# design_id -> Single Store API returns a list of designs. Pick any design_id you like
fetch_design = TeePublic::Api.designs(design_id).deep_symbolize_keys!
design = fetch_design[:body]
puts design
TeePublic prints each design on a variety of products like t-shirts, mugs and phone cases. Each product is of a certain product type, and each specific product (a combination of size, color, product type, etc) is represented by a single SKU
# design_id -> Single Store API returns a list of designs. Pick any design_id you like
# product_type -> Design API returns a list of skus, sku object has a key called "productType"
skus_url = "designs/#{design_id}/products/#{product_type}"
fetch_skus = TeePublic::Api.send(skus_url).deep_symbolize_keys!
skus = fetch_skus[:body]
puts skus
Affiliate Network ID is a unique ID assigned to an application (like https://mywebsite.com) on TeePublic system. This is required by TeePublic during checkout to make sure it credit sales back to an affiliate network.
# store_url is explained in "Single Store API" section
fetch_store = TeePublic::Api.send(store_url).deep_symbolize_keys!
affiliate_network_id = fetch_store.dig(:headers, :'x-affiliatenetwork-id')
puts affiliate_network_id
Will need the 3 routes below: the storefront, the design, and the cart.
get '/stores/:id', to: 'application#store'
get '/stores/:id/designs/:product_type/:design_id', to: 'application#design'
get '/cart', to: 'application#cart'
Decide which URL application plans to dedicate to show teepublic pages and add them to the routes.rb
According to Step 5, add actions in controller to handle newly added routes.
Please refactor according to the application style, we added all logic into one function to make it easier to understand
def store
@page ||= params['page'].try(:to_i) || 1
store_url = "stores/#{params[:id]}?page=#{@page}"
store_url += "&album_id=#{params['album_id']}" if params['album_id'].present?
store_url += "&product_type=#{params['product_type']}" if params['product_type'].present?
fetch_store = TeePublic::Api.send(store_url).deep_symbolize_keys!
@store = fetch_store[:body]
end
def design
@page ||= params['page'].try(:to_i) || 1
store_url = "stores/#{params[:id]}?page=#{@page}"
store_url += "&album_id=#{params['album_id']}" if params['album_id'].present?
store_url += "&product_type=#{params['product_type']}" if params['product_type'].present?
fetch_store = TeePublic::Api.send(store_url).deep_symbolize_keys!
@store = fetch_store[:body]
fetch_design = TeePublic::Api.designs(params['design_id']).deep_symbolize_keys!
@design = fetch_design[:body]
skus_url = "designs/#{params['design_id']}/products/#{params['product_type']}"
fetch_skus = TeePublic::Api.send(skus_url).deep_symbolize_keys!
@skus = fetch_skus[:body]
# Set at the end since this requires a response.
@affiliate_network_id = fetch_store.dig(:headers, :'x-affiliatenetwork-id')
end
def cart
end
Create 3 corresponding views to actions added in step 6
Add store.html.erb
<%= react_component('MySiteStore', store: @store, page: @page, albumId: params[:album_id], productTypeName: params[:product_type]) %>
Add design.html.erb
<%= react_component('MySiteDesign', store: @store, design: @design, skuData: @skus, affiliateNetworkId: @affiliate_network_id) %>
Add cart.html.erb
<%= react_component('MySiteCart') %>
Install the latest version of teepublic-react package
npm i teepublic-react
import teepublic-react css to your application
import 'teepublic-react/build/css/index.css'
Component level documentation could be found below in documentation section.
As noticed in the Step 7, some components (MySiteStore
, MySiteDesign
, MySiteCart
) which doesn't exist yet.
import React from 'react'
import { Footer, Store } from 'teepublic-react'
// We will talk about this in Step 9
import { TeePublicRoutes } from '../TeePublicConfiguration'
export default class MySiteStore extends React.Component {
render () {
const { store, page, albumId, productTypeName } = this.props
return (
<div className='teepublic store'>
<Store
className='contain'
storeData={store}
configuration={TeePublicRoutes}
selectedAlbumId={albumId}
selectedPage={page}
selectedProductTypeName={productTypeName || ''}
/>
<Footer />
</div>
)
}
}
import React, {Component} from 'react'
import { BuyProduct, CartButton, Footer } from 'teepublic-react'
// We will talk about this in Step 9
import { TeePublicRoutes } from '../TeePublicConfiguration'
export default class MySiteDesign extends Component {
render () {
const { design, store, skuData, affiliateNetworkId } = this.props
return (
<div className='teepublic design'>
<CartButton className='contain' href={TeePublicRoutes.cartUrl()} onCheckout={TeePublicRoutes.onCheckout} />
<BuyProduct
design={design}
store={store}
skuData={skuData}
storeUrl={TeePublicRoutes.storeUrl}
buyProductLinkBuilder={TeePublicRoutes.buyProductUrl}
tagLinkBuilder={TeePublicRoutes.tagUrl}
affiliateNetworkId={affiliateNetworkId}
className='contain'
/>
<Footer />
</div>
)
}
}
import React, {Component} from 'react'
import { Cart, Footer } from 'teepublic-react'
// We will talk about this in Step 9
import { TeePublicRoutes } from '../TeePublicConfiguration'
export default class MySiteCart extends Component {
render () {
return (
<div className='teepublic cart'>
<Cart
className='contain'
onCheckout={TeePublicRoutes.onCheckout}
storePathLinkBuilder={TeePublicRoutes.storesUrl}
buyProductLinkBuilder={TeePublicRoutes.buyProductUrl}
/>
<Footer />
</div>
)
}
}
Each application can host teepublic store, product or cart pages according to their routing convention. For eg. https://website.com/stores/store_id
https://website.com/design/design_id
React components allows applications to pass functions which knows how to build your application specific routes. It would be a good practice if application maintains these route builders in one file.
Create a TeepublicConfiguration.js
in the javascript folder and copy paste the content below and change the functions according to your application's URLs for TeePublic pages.
export const TeePublicRoutes = {
### NEEDS CHANGE ####
storeUrl: (storeId, selectedPage, selectedAlbumId, selectedProductTypeName) => {
let url = `/stores/${storeId}?page=${selectedPage || 1}`;
if (selectedAlbumId) url += `&album_id=${selectedAlbumId}`;
if (selectedProductTypeName) url += `&product_type=${selectedProductTypeName}`;
return url;
},
### NEEDS CHANGE ####
buyProductUrl: (designId, productType, storeId) => {
return `/stores/${storeId}/designs/${productType}/${designId}`;
},
### NEEDS CHANGE ####
cartUrl: () => {
return '/cart';
},
// Keep the `tagUrl` and `onCheckout` function as it is for now
// they will just work without any change.
tagUrl: (type, tag, affiliateId, affiliateNetworkId) => {
var baseUrl = 'https://www.teepublic.com'
let aff_param = `ref_id=${affiliateId || ''}`;
let aff_network_param = `aff_network_id=${affiliateNetworkId || ''}`;
return `${baseUrl}/${type}/${tag}?${aff_param}&${aff_network_param}`;
},
onCheckout: (cartItems) => {
const cartJSON = JSON.stringify({cartItems: cartItems})
var url = 'https://www.teepublic.com/external_cart?cart_items='
url += cartJSON
window.location = url
}
}
This component allows you to add items to a cart that is managed in localStorage. We read from the same localStorage object in the <Cart />
component
<AddToCart /> -----> localStorage -------> <Cart />
The AddToCart button could be placed anywhere on the screen just make sure you provide the required propTypes to this component.
We currently use this Component on <BuyProduct />
component where user can look through product details and if they like it they can "AddToCart"
AddToCart.propTypes = {
className: PropTypes.string,
design: PropTypes.object.isRequired,
affiliateId: PropTypes.number.isRequired,
affiliateNetworkId: PropTypes.number.isRequired,
storeId: PropTypes.number.isRequired,
sku: PropTypes.object.isRequired
};
design: design object returned from TeePublic APIs, there are several APIs which return design objects, one example is this. (https://api.teepublic.com/v1/docs#/designs/getV1DesignsId)
affiliateId: The Affiliate ID of a given store so that we can track the sale and credit your portion of the revenue to you account. You can find this (https://api.teepublic.com/v1/docs#/stores/getV1StoresId)
affiliateNetworkId: Your Affiliate Network ID so we can track the sale and credit your portion of the revenue to the account. Since it is possible to have multiple affiliates, and affiliate stores in your network, the Affiliate Network ID is necessary to get credit for sales coming from stores in your network. Found as a header X-AffiliateNetwork-Id
on API responses.
storeId: store id on TeePublic, also returned from the API (https://api.teepublic.com/v1/docs#/stores/getV1StoresId)
sku: Sku is unique identifier for product, this API (https://api.teepublic.com/v1/docs#/designs/getV1DesignsId) returns a list of sku
This renders a button which lets users go back to store page from anywhere. We use this component on <BuyProduct />
page.
BackToProducts.propTypes = {
className: PropTypes.string,
storeUrl: PropTypes.string.isRequired,
linkText: PropTypes.string.isRequired,
};
storeUrl: Application specific store URL like 'https://my-website.com/teepublic/stores/store-slug'
linkText: Configure button text like "Back to Store"
This render the product page for a given design. Users can select different style, color or size of the product in this component.
Other Components it uses internally
AddToCart -> Allows users to add current selected sku to the cart
ImageGallery -> As it sounds, it shows the mockups of design
BackToProducts -> This allows user to go back to store page
ProductVariants -> This shows users different product types for the same design
SkuSelector -> A design has a complex rules when a particular option is selected like size and color
RelatedTags -> This shows users related tags to the current design which are calcauted by our Graph DB
BuyProduct.propTypes = {
className: PropTypes.string,
design: PropTypes.object.isRequired,
store: PropTypes.object.isRequired,
storeUrl: PropTypes.func.isRequired,
affiliateNetworkId: PropTypes.number.isRequired,
tagLinkBuilder: PropTypes.func.isRequired,
buyProductLinkBuilder: PropTypes.func.isRequired,
sizechartUrl: PropTypes.func.isRequired
};
design: design object returned from TeePublic APIs (https://api.teepublic.com/v1/docs#/designs/getV1DesignsId)
store: store object is returned by TeePublic APIs (https://api.teepublic.com/v1/docs#/stores/getV1StoresId)
storeUrl: Function to build application specific URL for store so that <BackToProducts />
could take users back to the store.
(storeId) => {
return `/stores/${storeId}`;
}
affiliateNetworkId: Your Affiliate Network ID so we can track the sale and credit your portion of the revenue to the account. Since it is possible to have multiple affiliates, and affiliate stores in your network, the Affiliate Network ID is necessary to get credit for sales coming from stores in your network. Found as a header X-AffiliateNetwork-Id
on API responses.
tagLinkBuilder: There is component at the bottom of the page to allow users dive into other related tags. But in order to make that happen we need a URL where we can direct them after the click. This is how our demo projects looks like.
tagLinkBuilder={(type, tag, affiliateId, affiliateNetworkId) => {
var baseUrl = 'https://www.teepublic.com';
return `${baseUrl}/${type}/${tag}?ref_id=${affiliateId}&aff_network_id=${affiliateNetworkId}`;
}}
For the time being you would have to use same baseUrl as mentioned above since we don't have capability to show designs for tags on white label stores.
buyProductLinkBuilder: On Buy Product page, we allow users to buy other products too like same designs on different products or different designs from your store. So in order to route them properly we need to understand how application specific BuyProduct Urls looks like.
(designId, productTypeName, storeId) => {
return `/stores/${storeId}/designs/${designId}?product_type=${productTypeName}`;
}
productTypeName is available in design objects
sizechartUrl: This helps user to look at size chart
() => {
return 'https://www.teepublic.com/sizechart';
}
Reads the added cart items from local storage to populate Cart and show it to users.
Cart.propTypes = {
className: PropTypes.string,
storePathLinkBuilder: PropTypes.func.isRequired,
buyProductLinkBuilder: PropTypes.func.isRequired,
onCheckout: PropTypes.func.isRequired
};
storePathLinkBuilder: A function to build your application specific store URLs.
(
storeId,
selectedPage,
selectedAlbumId,
selectedProductTypeName
) => {
let url = `/stores/${storeId}?page=${selectedPage}`;
if (selectedAlbumId) url += `&album_id=${selectedAlbumId}`;
if (selectedProductTypeName)
url += `&product_type=${selectedProductTypeName}`;
return url;
}
buyProductLinkBuilder: A function to build BuyProduct page URL which let's users to go back to product page from the items in the cart
(designId, productTypeName, storeId) => {
return `/stores/${storeId}/designs/${designId}?product_type=${productTypeName}`;
}
onCheckout: A function to help users to checkout and redirect them to TeePublic. Below is a working example
(cartItems) => {
const cartJSON = JSON.stringify({cartItems: cartItems})
var url = 'https://www.teepublic.com/external_cart?cart_items='
url += cartJSON
window.location = url
},
Cart Button should be placed at all times on your UI. It does three things.
- keep poling localStorage for any changes and if there is any item added to the cart from anywhere, it will just update the count
- Allow users to click on the cart to see their full cart and what's in it
- Direct Checkout and skip the looking at the cart part
Cart.propTypes = {
className: PropTypes.string,
cartUrl: PropTypes.string.isRequired,
onCheckout: PropTypes.func.isRequired
};
cartUrl: Application specific URL where <Cart />
component is rendered
onCheckout: A function to help users to checkout and redirect them to TeePublic. Below is a working example
(cartItems) => {
const cartJSON = JSON.stringify({cartItems: cartItems})
var url = 'https://www.teepublic.com/external_cart?cart_items='
url += cartJSON
window.location = url
},
This component is being used by <Cart />
internally to show individuals cart item.
CartItem.propTypes = {
className: PropTypes.string,
cartItem: PropTypes.object.isRequired,
buyProductLinkBuilderL PropTypes.func.isRequired,
updateCartItem: PropTypes.func.isRequired,
deleteCartItem: PropTypes.func.isRequired
};
This is a wrapper around flex box with column layout
This component shows a collection of <DesignTile />
. This component is being used inside <Store />
component but if applications needs to show a collection of designs somewhere else, this could be used.
DesignCollection.propTypes = {
designs: PropTypes.array.isRequired,
buyProductLinkBuilder: PropTypes.func.isRequired,
storeId: PropTypes.number
};
designs: List of designs returned from TeePublic APIs. (https://api.teepublic.com/v1/docs#/designs/getV1Designs)
buyProductLinkBuilder: It allows design collection to build your application specific urls to render <BuyProduct />
pages.
(designId, productTypeName, storeId) => {
return `/stores/${storeId}/designs/${designId}?product_type=${productTypeName}`;
}
storeId: This is the store id on TeePublic, returned from the API (https://api.teepublic.com/v1/docs#/stores/getV1StoresId)
This component is responsible for rendering individual design tile which includes product mockup, price, design title and a buy button.
DesignTile.propTypes = {
className: PropTypes.string,
design: PropTypes.object.isRequired,
buyProductLinkBuilder: PropTypes.func.isRequired,
storeId: PropTypes.number.isRequired
};
design: a single design object returned from TeePublic APIs (https://api.teepublic.com/v1/docs#/designs/getV1Designs)
buyProductLinkBuilder: It allows design tile to build your application specific urls to render <BuyProduct />
pages.
(designId, productTypeName, storeId) => {
return `/stores/${storeId}/designs/${designId}?product_type=${productTypeName}`;
}
storeId: This is your store id on TeePublic, also returned from the API (https://api.teepublic.com/v1/docs#/stores/getV1StoresId)
This render a footer component with powered by TeePublic logo in it.
This component is being used internally by <BuyProduct />
component, which allows users to see through different images of the product.
ImageGallery.propTypes = {
className: PropTypes.string,
images: PropTypes.array.isRequired,
selectedImageIndex: PropTypes.number.isRequired,
onImageSelect: PropTypes.func.isRequired
};
images: Array of image urls that will help the component to build thumbnails. https://api.teepublic.com/v1/docs#/designs/getV1Designs. design -> skus -> images
selectedImageIndex: Parent should pass selectedImageIndex, so the component knows which image to load on the main image.
onImageSelect: Since this component doesn't maintain the selectedImageIndex, we expect a function to be passed, components fires when a user clicks on a thumbnail. For eg, if users clicks on the 3rd image in the images array, component is gonna call onImageSelect with index = 2.
It renders a pagination component which allows users to change page number of something.
If the number of designs are too large to load in one go, application will need to implement paging by passing paging params to this API (https://api.teepublic.com/v1/docs#/designs/getV1Designs). This component is used in <Store />
component to manage different pages.
Pagination.propTypes = {
className: PropTypes.string,
currentPage: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
totalPages: PropTypes.number.isRequired
};
currentPage: Currently selected page, we let parent component store the state of currentPage
onPageChange: This is a function which is called when users clicks a page.
totalPages: Number of pages parents wants to render
It renders a collection of product for a same design. Each design can be printed on different product types like t-shirt, mug or iPhone case. This helps an application to show different product variants for the same design.
ProductVariants.propTypes = {
className: PropTypes.string,
design: PropTypes.object.isRequired,
store: PropTypes.object.isRequired,
currentSku: PropTypes.object.isRequired,
buyProductLinkBuilder: PropTypes.func.isRequired
};
design: design object for which an application want to show different product variants.
store: store object as received from the TeePublic APIs (https://api.teepublic.com/v1/docs#/stores/getV1StoresId)
currentSku: This helps the component show everything other than the current sku. design object -> skus
buyProductLinkBuilder: It allows product variants to build your application specific urls to render <BuyProduct />
pages if users clicks on any of those product variants.
(designId, productTypeName, storeId) => {
return `/stores/${storeId}/designs/${designId}?product_type=${productTypeName}`;
}
This renders a list of clickable tags related to a design. This could be found at the bottom of <BuyProduct />
component where it shows other related tags for the current design.
RelatedTags.propTypes = {
className: PropTypes.string,
design: PropTypes.object.isRequired,
tagLinkBuilder: PropTypes.func.isRequired,
affiliateId: PropTypes.number,
affiliateNetworkId: PropTypes.number
};
This is a wrapper around flex box with row layout
This renders a complete store page very similar to your TeePublic store.
Store.propTypes = {
className: PropTypes.string,
storeData: PropTypes.object.isRequired,
configuration: PropTypes.shape({
storeUrl: PropTypes.func.isRequired,
cartUrl: PropTypes.func.isRequired,
buyProductUrl: PropTypes.func.isRequired,
onCheckout: PropTypes.func.isRequired,
}).isRequired,
selectedAlbumId: PropTypes.number,
selectedPage: PropTypes.number,
selectedProductTypeName: PropTypes.string
};
storeData: It's the store object returned from https://api.teepublic.com/v1/docs#/stores/getV1StoresId
storeUrl: This is a function to build application specific store URLs and this a demo function
(storeId) => {
return `/stores/${storeId}`;
}
cartUrl: This is a function which return url to the page where <Cart />
component is being rendered.
buyProductUrl: This is a function to build your application specific URL to <BuyProduct />
component
(designId, productTypeName, storeId) => {
return `/stores/${storeId}/designs/${designId}?product_type=${productTypeName}`;
}
onCheckout: A function to help users to checkout and redirect them to TeePublic. Below is a working example
(cartItems) => {
const cartJSON = JSON.stringify({cartItems: cartItems})
var url = 'https://www.teepublic.com/external_cart?cart_items='
url += cartJSON
window.location = url
},
selectedAlbumId: The ID of the currently selected album, if there is an album selected. This should get passed down to the <StoreFilter />
.
selectedPage: The currently selected page number; used for paging. This should get passed down to the <StoreFilter />
.
selectedProductTypeName: The currently selected product type. This should get passed down to the <StoreFilter />
to enable filtering by that product.
It renders a component to show different set of options to filter a store. It's being used inside <Store />
component.
StoreFilter.propTypes = {
className: PropTypes.string,
albums: PropTypes.array.isRequired,
productTypes: PropTypes.array.isRequired,
selectedAlbumId: PropTypes.number,
selectedProductTypeName: PropTypes.string,
onAlbumChange: PropTypes.func.isRequired,
onProductTypeChange: PropTypes.func.isRequired,
filterStyle: PropTypes.oneOf(['nav', 'dropdown']),
};
albums: List of albums could be retrieved from this TeePublic API (https://api.teepublic.com/v1/docs#/stores/getV1StoresId) look for albums object
productTypes: List of product types could be retrieved from this TeePublic API (https://api.teepublic.com/v1/docs#/stores/getV1StoresId), look for productTypes and could be passed as it is
selectedAlbumId: Send a selected album id to the component so the component can select album id visually
selectedProductTypeName: Same logic as selectedAlbumId
onAlbumChange: This is a function called when a user clicks on any album to notify the parent component to make an another request to TeePublic API to filter by album
onProductTypeChange: Same logic as onAlbumChange
filterStyle: Component support two styles as of now. "nav" and "dropdown". dropdown is a default style. Dropdown is designed for mobile and nav is for desktop
This renders a logo saying Powered By TeePublic
className You can pass className as a prop to any of TeePublic component and it will get added to the parent tag of the component
// We have some code like this in each component
import classnames from 'classnames';
const { className } = this.props;
const classes = classnames(CLASS_ROOT, className);
CLASS_ROOT: Each component have a CLASS_ROOT defined inside it which is of tp-[component]
form.
PropTypes: Every component has it's expected PropTypes defined at the bottom of the [component].js
file with proper requirements like what kind and if it is required. It should be reliable for most parts but this shouldn't be your source of truth.
Clone the repo on your dev machine
git clone https://github.com/BustedTees/teepublic-react
npm install
to install all dependencies
npm start
to run the demo project
npm publish
to publish the package on npm. You would need to change the version number in package.json
git push -u origin <branch-name>
& create PR and get it reviewed