Skip to content

Instantly share code, notes, and snippets.

@daronspence
Created April 30, 2020 18:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save daronspence/83a44ea4ccd2b9461898f7a2028eeb55 to your computer and use it in GitHub Desktop.
Save daronspence/83a44ea4ccd2b9461898f7a2028eeb55 to your computer and use it in GitHub Desktop.
Contentful
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