Created
March 12, 2021 18:54
-
-
Save good-idea/22f7efb47ffc5f2e09d0326c9cf62ca5 to your computer and use it in GitHub Desktop.
Sanity Related Select
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
export const getArticleImage = { | |
name: 'getArticleImage', | |
title: 'Get Article Image', | |
type: 'string', | |
inputComponent: GetArticleImage | |
}; |
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 * 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