-
-
Save daronspence/83a44ea4ccd2b9461898f7a2028eeb55 to your computer and use it in GitHub Desktop.
Contentful
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react'; | |
import PropTypes from 'prop-types'; | |
import ReactDOM from 'react-dom'; | |
import { TextInput } from '@contentful/forma-36-react-components'; | |
import { init } from 'contentful-ui-extensions-sdk'; | |
import '@contentful/forma-36-react-components/dist/styles.css'; | |
import './dist.css'; | |
import { createApolloFetch } from 'apollo-fetch'; | |
import { debounce, union, keyBy } from 'lodash-es'; | |
import { sortableContainer, sortableElement } from 'react-sortable-hoc'; | |
import arrayMove from 'array-move'; | |
import SearchHeader from './components/SearchHeader'; | |
const SortableContainer = sortableContainer(({ children }) => { | |
return <div>{children}</div>; | |
}); | |
const SortableItem = sortableElement(({ children, props }) => <div {...props}>{children}</div>); | |
export class App extends React.Component { | |
static propTypes = { | |
sdk: PropTypes.object.isRequired | |
}; | |
graphqlUri = 'https://cm-gql-plnmiq5ora-uc.a.run.app/graphql'; | |
api = createApolloFetch({ uri: this.graphqlUri }); | |
detachExternalChangeHandler = null; | |
constructor(props) { | |
super(props); | |
this.state = { | |
value: [], | |
searchQuery: '', | |
searchItems: [], | |
cachedItems: {} | |
}; | |
} | |
componentDidMount() { | |
this.props.sdk.window.startAutoResizer(); | |
// Handler for external field value changes (e.g. when multiple authors are working on the same entry). | |
this.detachExternalChangeHandler = this.props.sdk.field.onValueChanged(this.onExternalChange); | |
} | |
componentWillMount() { | |
let fieldValue = this.props.sdk.field.getValue(); | |
if (typeof fieldValue === 'undefined') { | |
fieldValue = []; | |
} | |
this.setState({ value: fieldValue }); | |
fieldValue.map(item => { | |
const q = parseInt(item); | |
const query = `query { | |
product( | |
productId: ${q}, | |
){ | |
title | |
base_image_url | |
product_id | |
} | |
}`; | |
this.api({ query }) | |
.then(({ data }) => { | |
const { product } = data; | |
this.addItemToCache(product); | |
}) | |
.catch(err => { | |
console.log(err); | |
}); | |
}); | |
} | |
componentWillUnmount() { | |
if (this.detachExternalChangeHandler) { | |
this.detachExternalChangeHandler(); | |
} | |
} | |
onExternalChange = value => { | |
if (this.state.value === value) { | |
return; | |
} | |
if (typeof value === 'undefined') { | |
value = []; | |
} | |
this.setState({ value }); | |
}; | |
addItemToCache(item) { | |
this.setState(state => { | |
const items = [item]; | |
const cache = Object.values(state.cachedItems); | |
return { | |
cachedItems: keyBy(union(items, cache), 'product_id') | |
}; | |
}); | |
} | |
addItem = productId => { | |
this.setState(state => { | |
const value = state.value; | |
value.push(productId.trim()); | |
return { | |
value, | |
searchQuery: '' | |
}; | |
}); | |
}; | |
searchForItems = debounce(q => { | |
const query = `query { | |
productSearch( | |
q: "${q}", | |
pagination: { | |
page: 1, | |
per_page: 10 | |
} | |
){ | |
products { | |
title | |
base_image_url | |
product_id | |
} | |
} | |
}`; | |
this.api({ query }) | |
.then(({ data }) => { | |
if (data === null) { | |
this.setState({ searchItems: [] }); | |
} else { | |
this.setState({ searchItems: data.productSearch.products }); | |
this.setState(state => { | |
const items = data.productSearch.products; | |
const cache = Object.values(state.cachedItems); | |
return { | |
cachedItems: keyBy(union(items, cache), 'product_id') | |
}; | |
}); | |
} | |
setTimeout(this.props.sdk.window.updateHeight(), 2000); | |
}) | |
.catch(err => { | |
console.log(err); | |
this.setState({ searchItems: [] }); | |
}); | |
}, 400); | |
removeItem = index => { | |
this.setState(state => { | |
const value = state.value; | |
value.splice(index, 1); | |
return { value }; | |
}); | |
}; | |
onChangeNewItem = e => { | |
const searchQuery = e.currentTarget.value; | |
this.setState({ searchQuery }); | |
}; | |
componentDidUpdate(prevProps, prevState) { | |
const old = prevState.value; | |
const newVal = this.state.value; | |
this.props.sdk.field.setValue(newVal); | |
if (newVal !== old) { | |
this.props.sdk.field | |
.setValue(newVal) | |
.then(data => { | |
console.log('did update data', data); | |
}) | |
.catch(err => { | |
console.log(err); | |
}); | |
} | |
} | |
onSortEnd = ({ oldIndex, newIndex }) => { | |
this.setState(({ value }) => ({ | |
value: arrayMove(value, oldIndex, newIndex) | |
})); | |
}; | |
render() { | |
return ( | |
<div> | |
{!this.state.value.length && ( | |
<div className="text-lg uppercase font-bold">Add some products!</div> | |
)} | |
<SortableContainer items={this.state.value} onSortEnd={this.onSortEnd}> | |
{this.state.value.map((item, index) => { | |
const product = this.state.cachedItems[item]; | |
const imageUrl = product | |
? product.base_image_url | |
: `https://s7d5.scene7.com/is/image/CentralMarket/000000002-1?$large$&hei=324&wid=324`; | |
return ( | |
<SortableItem key={index} index={index}> | |
<div className="flex items-center mt-2 bg-gray-200 rounded-lg p-2 cursor-move"> | |
<img alt={item} className="w-12 h-12 object-fit rounded-full" src={imageUrl} /> | |
<div className="mx-8 flex-1">{product && product.title}</div> | |
<button onClick={() => this.removeItem(index)} className="p-2"> | |
<span aria-label="remove" role="img" className="pointer-events-none"> | |
❌ | |
</span> | |
</button> | |
</div> | |
</SortableItem> | |
); | |
})} | |
<p className="mt-1 flex items-center justify-end"> | |
<span role="img" aria-label="package"> | |
📦 | |
</span> | |
<span className="ml-2 uppercase text-sm leading-none">Drag to sort</span> | |
</p> | |
</SortableContainer> | |
<hr className="mt-4" /> | |
<TextInput | |
width="large" | |
type="text" | |
placeholder="Search for products" | |
className="mt-8" | |
value={this.state.searchQuery} | |
onChange={e => this.searchForItems(e.currentTarget.value)} | |
/> | |
<SearchHeader items={this.state.searchItems} /> | |
<div className="grid grid-cols-4"> | |
{this.state.searchItems.map(item => { | |
return ( | |
<div | |
key={item.product_id} | |
className="flex flex-col items-start mt-2 border-t pt-2 border-gray-300"> | |
<button | |
className="py-1 px-2 bg-green-300 font-bold uppercase text-sm" | |
onClick={() => this.addItem(item.product_id)}> | |
Add | |
</button> | |
<img | |
className="h-16 w-16 object-contain mx-4" | |
alt={item.title} | |
src={item.base_image_url} | |
/> | |
{item.title} | |
</div> | |
); | |
})} | |
</div> | |
</div> | |
); | |
} | |
} | |
init(sdk => { | |
ReactDOM.render(<App sdk={sdk} />, document.getElementById('root')); | |
}); | |
/** | |
* By default, iframe of the extension is fully reloaded on every save of a source file. | |
* If you want to use HMR (hot module reload) instead of full reload, uncomment the following lines | |
*/ | |
if (module.hot) { | |
module.hot.accept(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment