Skip to content

Instantly share code, notes, and snippets.

@good-idea
Created March 12, 2021 18:54
Show Gist options
  • Save good-idea/22f7efb47ffc5f2e09d0326c9cf62ca5 to your computer and use it in GitHub Desktop.
Save good-idea/22f7efb47ffc5f2e09d0326c9cf62ca5 to your computer and use it in GitHub Desktop.
Sanity Related Select
export const getArticleImage = {
name: 'getArticleImage',
title: 'Get Article Image',
type: 'string',
inputComponent: GetArticleImage
};
import * as React from 'react';
import { SanityDocument } from '@sanity/client';
import { withDocument } from 'part:@sanity/form-builder';
import { Stack, Select, Heading, Button, Text } from '@sanity/ui';
import { useDocumentOperation } from '@sanity/react-hooks';
import createImageUrlBuilder from '@sanity/image-url';
import client from '../utils/client';
import { definitely } from '../utils/definitely';
const imageBuilder = createImageUrlBuilder(client);
const { useReducer, useEffect, useCallback } = React;
interface ImageSizes {
width: number;
height: number;
}
const getImageUrl = (image: any, { width, height }: ImageSizes): string =>
imageBuilder
.image(image)
.width(width)
.height(height)
.url();
enum ActionTypes {
INIT = 'INIT',
INVALID = 'INVALID',
READY = 'READY',
SELECT_ARTICLE = 'SELECT_ARTICLE',
SUBMIT = 'SUBMIT',
SUCCESS = 'SUCCESS',
WARN = 'WARN'
}
interface State {
loading: boolean;
articles: SanityDocument[];
selectedArticle: SanityDocument | null;
message: string | null;
messageIsError: boolean;
hideInput: boolean;
success: boolean;
}
interface InitAction {
type: ActionTypes.INIT;
}
interface InvalidAction {
type: ActionTypes.INVALID;
message: string;
messageIsError: boolean;
hideInput: boolean;
}
interface ReadyAction {
type: ActionTypes.READY;
articles: SanityDocument[];
}
interface SelectArticleAction {
type: ActionTypes.SELECT_ARTICLE;
article: SanityDocument;
}
interface SubmitAction {
type: ActionTypes.SUBMIT;
}
interface SuccessAction {
type: ActionTypes.SUCCESS;
}
interface WarningAction {
type: ActionTypes.WARN;
message: string;
}
type Action =
| InitAction
| InvalidAction
| ReadyAction
| SelectArticleAction
| SubmitAction
| SuccessAction
| WarningAction;
const initialState: State = {
loading: true,
articles: [],
selectedArticle: null,
message: null,
messageIsError: false,
hideInput: false,
success: false
};
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case ActionTypes.INIT:
return {
...initialState
};
case ActionTypes.READY:
return {
...state,
loading: false,
articles: action.articles,
selectedArticle: action.articles.length ? action.articles[0] : null,
messageIsError: false
};
case ActionTypes.INVALID:
return {
...state,
loading: false,
message: action.message,
messageIsError: action.messageIsError,
hideInput: action.hideInput
};
case ActionTypes.SELECT_ARTICLE:
return {
...state,
selectedArticle: action.article
};
case ActionTypes.SUBMIT:
return {
...state,
message: null,
messageIsError: false,
loading: true
};
case ActionTypes.SUCCESS:
return {
...state,
loading: false,
message: '🎉 Successfully copied image',
messageIsError: false,
success: true,
hideInput: true
};
case ActionTypes.WARN:
return {
...state,
message: action.message,
messageIsError: true
};
default:
return state;
}
};
interface GetArticleImageProps {
document: SanityDocument;
}
const innerStyles = {
display: 'grid',
gridTemplateColumns: '350px 40px auto',
margin: '15px 0',
gridGap: '20px'
};
const imgPreviewStyles = {
width: '40px',
height: '40px',
backgroundColor: '#EEE'
};
const imgStyles = {
width: '100%',
height: '100%',
objectFit: 'cover' as 'cover'
};
export const GetArticleImageBase: React.FC<GetArticleImageProps> = props => {
const { document } = props;
// @ts-ignore
const { patch } = useDocumentOperation(
document._id.replace(/^drafts\./, ''),
document._type
);
const [state, dispatch] = useReducer(reducer, initialState);
const {
loading,
articles,
selectedArticle,
hideInput,
message,
messageIsError,
success
} = state;
const isDisabled = loading;
const loadArticles = useCallback(async () => {
const relatedArticles = definitely(document?.relatedArticles);
// @ts-ignore
const articleIds = relatedArticles.map(article => article._ref);
const articles = await client.fetch<SanityDocument[]>(
`*[_type == "article" && _id in $articleIds]{
_id,
title,
heroImage,
featuredImage,
"heroImageAsset": heroImage.asset->,
"featuredImageAsset": featuredImage.asset->,
}`,
{ articleIds }
);
dispatch({ type: ActionTypes.READY, articles });
}, [document]);
/**
* Initialize
*/
useEffect(() => {
if (success) return;
if (document?.heroImage?.asset) {
dispatch({
type: ActionTypes.INVALID,
message:
'This recipe already has an image. To use this tool, remove the Hero Image.',
messageIsError: false,
hideInput: true
});
return;
}
if (definitely(document?.relatedArticles).length === 0) {
dispatch({
type: ActionTypes.INVALID,
message:
'This recipe does not yet have any related articles. Assign these relationships in the Article documents.',
messageIsError: true,
hideInput: false
});
return;
}
dispatch({ type: ActionTypes.INIT });
loadArticles();
}, [document, loadArticles, success]);
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const article = articles.find(a => a._id === e.target.value);
dispatch({ type: ActionTypes.SELECT_ARTICLE, article });
};
const handleSubmit = () => {
dispatch({ type: ActionTypes.SUBMIT });
const newImage =
selectedArticle?.heroImage || selectedArticle?.featuredImage;
if (!newImage) {
dispatch({
type: ActionTypes.WARN,
message: `The article "${selectedArticle.title}" does not have a hero or featured image`
});
return;
}
const imagePatch = { set: { heroImage: newImage } };
patch.execute([imagePatch]);
dispatch({ type: ActionTypes.SUCCESS });
};
const selectedArticleImage = selectedArticle
? selectedArticle.heroImageAsset || selectedArticle.featuredImageAsset
: null;
const imageSrc = selectedArticleImage
? getImageUrl(selectedArticleImage, { width: 40, height: 40 })
: null;
const messageStyles = {
color: messageIsError ? '#ce4545' : undefined,
fontStyle: messageIsError ? 'italic' : undefined
};
const buttonTone = 'default';
return (
<Stack space={4}>
<Heading as="h4" size={0}>
🛠 Get Image from Related Article
</Heading>
{message ? (
<Text size={1} style={messageStyles} muted={messageIsError !== true}>
{message}
</Text>
) : null}
{hideInput !== true ? (
<div style={innerStyles}>
<Select
height="40px"
disabled={isDisabled || articles.length === 0}
onChange={handleChange}
value={selectedArticle?._id}
fontSize={5}
>
{articles.map(article => (
<option key={article._id} value={article._id}>
{article.title}
</option>
))}
</Select>
<div style={imgPreviewStyles}>
{imageSrc ? (
<img
alt={selectedArticle.title}
style={imgStyles}
src={imageSrc}
/>
) : null}
</div>
<Button
type="button"
disabled={isDisabled || !selectedArticle}
onClick={handleSubmit}
fontSize={5}
tone={buttonTone}
text="Copy Image"
/>
</div>
) : null}
</Stack>
);
};
export const GetArticleImage = withDocument(GetArticleImageBase);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment