-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from nervina-labs/feat/image
feat: Add image
- Loading branch information
Showing
15 changed files
with
519 additions
and
214 deletions.
There are no files selected for viewing
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { Story, Meta } from '@storybook/react' | ||
import { | ||
Image as MibaoImage, | ||
MibaoProvider | ||
} from 'mibao-ui' | ||
import { Spinner, Stack } from '@chakra-ui/react' | ||
|
||
export default { | ||
component: MibaoImage, | ||
title: 'Components/Image', | ||
argTypes: { | ||
aspectRatio: { | ||
defaultValue: true | ||
}, | ||
objectFit: { | ||
options: ['fill', 'contain', 'cover', 'none', 'scale-down'], | ||
control: { type: 'select' } | ||
}, | ||
loading: { | ||
options: ['', 'eager', 'lazy'], | ||
control: { type: 'select' } | ||
}, | ||
crossOrigin: { | ||
options: ['', 'anonymous', 'use-credentials'], | ||
control: { type: 'select' } | ||
} | ||
}, | ||
parameters: { | ||
fallback: 'ReactNode' | ||
} | ||
} as Meta | ||
|
||
const Template: Story = (args) => | ||
<MibaoProvider> | ||
<Stack spacing={3} direction="row"> | ||
<MibaoImage {...args} /> | ||
<MibaoImage {...args} loader={<Spinner color="primary.600" emptyColor="gray.200" m="auto" />} /> | ||
<MibaoImage {...args} fallback={<div>Failed</div>} /> | ||
</Stack> | ||
</MibaoProvider> | ||
|
||
export const Image = Template.bind({}) | ||
Image.args = { | ||
src: 'https://oss.jinse.cc/production/bd7c7508-026e-4fcd-abf8-1fa1061f4d6c.png', | ||
alt: '', | ||
aspectRatio: true, | ||
width: '200px', | ||
height: '', | ||
objectFit: 'cover', | ||
crossOrigin: '', | ||
disableContextMenu: false, | ||
resizeScale: 200 | ||
} |
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
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const OSS_IMG_PROCESS_QUERY_KEY = process.env.OSS_IMG_PROCESS_QUERY_KEY ?? 'x-oss-process' | ||
export const OSS_IMG_PROCESS_QUERY_KEY_SCALE = process.env.OSS_IMG_PROCESS_QUERY_KEY_SCALE ?? 'image/resize,s_' | ||
export const OSS_IMG_PROCESS_QUERY_KEY_FORMAT_WEBP = process.env.OSS_IMG_PROCESS_QUERY_KEY_FORMAT_WEBP ?? '/format,webp' | ||
export const OSS_IMG_HOSTS = process.env.OSS_IMG_HOSTS | ||
? process.env.OSS_IMG_HOSTS.split(',') | ||
: [ | ||
'https://oss.jinse.cc', | ||
'https://goldenlegend.oss-accelerate.aliyuncs.com' | ||
] |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
.image { | ||
} | ||
|
||
.image[hide="true"] { | ||
opacity: 0; | ||
pointer-events: none; | ||
} | ||
|
||
.image[data-aspect-ratio="true"] { | ||
aspect-ratio: 1/1; | ||
} | ||
|
||
.loading { | ||
width: 100%; | ||
height: 100%; | ||
position: absolute; | ||
top: 0; | ||
left: 0; | ||
display: flex; | ||
} | ||
|
||
.container { | ||
position: relative; | ||
width: auto; | ||
height: auto; | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { render } from '@testing-library/react' | ||
|
||
import { Image } from './image' | ||
|
||
describe('Image', () => { | ||
it('should render successfully', () => { | ||
const { baseElement } = render(<Image />) | ||
expect(baseElement).toBeTruthy() | ||
}) | ||
}) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import { Box, Image as ChakraImage, ImageProps as ChakraImageProps, Skeleton } from '@chakra-ui/react' | ||
import styles from './image.module.scss' | ||
import { useState, useMemo, useEffect, useCallback, ReactNode } from 'react' | ||
import FALLBACK_SRC from '../../../assets/images/fallback.svg' | ||
import { addParamsToUrl, disableImageContext, getImagePreviewUrl, omit } from '../../utils' | ||
|
||
export interface ImageProps extends ChakraImageProps { | ||
aspectRatio?: boolean | ||
loader?: ReactNode | ||
srcQueryParams?: Record<string, string | number> | ||
disableContextMenu?: boolean | ||
resizeScale?: number // Specifies the shortest edge of the target zoom graph. | ||
} | ||
|
||
export const Image = (props: ImageProps) => { | ||
const { fallbackSrc = FALLBACK_SRC } = props | ||
const [isLoading, setIsLoading] = useState<boolean>(true) | ||
const [isError, setIsError] = useState(false) | ||
// loading element | ||
const loaderEl = useMemo(() => { | ||
if (props.loader) return props.loader | ||
return <Skeleton width="100%" height="100%" rounded={props.rounded} /> | ||
}, [props.loader, props.rounded]) | ||
// src | ||
const imageSrc = useMemo(() => { | ||
if (!props.src) return props.src | ||
const srcQueryParams = props.srcQueryParams ?? {} | ||
if (props.resizeScale) { | ||
return addParamsToUrl(getImagePreviewUrl(props.src, props.resizeScale), srcQueryParams) | ||
} | ||
return addParamsToUrl(props.src, srcQueryParams) | ||
}, [props]) | ||
// omit props | ||
const imageProps = useMemo(() => omit(props, [ | ||
'aspectRatio', | ||
'loader', | ||
'srcQueryParams', | ||
'resizeScale' | ||
]), [props]) | ||
|
||
useEffect(() => { | ||
if (props.src) { | ||
setIsLoading(true) | ||
setIsError(false) | ||
} | ||
}, [props.src]) | ||
|
||
const onLoaded = useCallback((event) => { | ||
setIsLoading(false) | ||
if (props.onLoad) props.onLoad(event) | ||
}, [props]) | ||
|
||
const onError = useCallback((event) => { | ||
setIsLoading(false) | ||
setIsError(true) | ||
if (props.onError) props.onError(event) | ||
}, [props]) | ||
|
||
return ( | ||
<Box className={styles.container} style={{ | ||
width: props.width as string, | ||
height: props.height as string | ||
}}> | ||
{ | ||
isLoading && ( | ||
<Box className={styles.loading}> | ||
{loaderEl} | ||
</Box> | ||
) | ||
} | ||
<ChakraImage | ||
{...imageProps} | ||
hide={!props.src} | ||
data-aspect-ratio={props.aspectRatio} | ||
src={imageSrc} | ||
className={`${styles.image} ${props.className ? props.className : ''}`} | ||
onLoad={onLoaded} | ||
onError={onError} | ||
objectFit={props.objectFit ?? 'cover'} | ||
htmlWidth={props.width as string} | ||
htmlHeight={props.height as string} | ||
fallbackSrc={isError ? fallbackSrc : undefined} | ||
fallback={isError ? props.fallback : undefined} | ||
onContextMenu={props.disableContextMenu ? disableImageContext : undefined} | ||
/> | ||
</Box> | ||
) | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { | ||
OSS_IMG_HOSTS, | ||
OSS_IMG_PROCESS_QUERY_KEY, | ||
OSS_IMG_PROCESS_QUERY_KEY_SCALE | ||
} from '../constants/env' | ||
|
||
export function addParamsToUrl ( | ||
url: string, | ||
params: Record<string, string | number>, | ||
options?: { | ||
ignoreDuplicates?: boolean | ||
} | ||
): string { | ||
if (!url) { | ||
return url | ||
} | ||
const urlObj = new URL(url) | ||
const urlSearchParams = urlObj.searchParams | ||
Object.keys(params).forEach((key) => { | ||
if (!urlSearchParams.has(key) || options?.ignoreDuplicates) { | ||
urlSearchParams.set(key, String(params[key])) | ||
} | ||
}) | ||
return decodeURI(urlObj.toString()) | ||
} | ||
|
||
export function getImagePreviewUrl<U extends string | undefined> ( | ||
url: U, | ||
size = 300 | ||
): U { | ||
if (!url) { | ||
return url | ||
} | ||
const urlObj = new URL(url) | ||
const isOssHost = OSS_IMG_HOSTS.some((host) => url?.startsWith(host)) | ||
const isSvgOrWebp = /\.(svg|webp)$/i.test(urlObj.pathname) | ||
if (!isOssHost || isSvgOrWebp) { | ||
return url | ||
} | ||
const params: Record<string, string | number> = {} | ||
params[OSS_IMG_PROCESS_QUERY_KEY] = `${OSS_IMG_PROCESS_QUERY_KEY_SCALE}${size}` | ||
return addParamsToUrl(url, params) as U | ||
} | ||
|
||
export function omit<T extends {[key: string]: any}, K extends keyof T> (obj: T, keys: K[]): Omit<T, K> { | ||
return keys.reduce((acc, key) => ({ ...acc, [key]: undefined }), obj) | ||
} | ||
|
||
export const disableImageContext = (e: any): boolean => { | ||
e?.preventDefault?.() | ||
e?.stopPropagation?.() | ||
return false | ||
} |
Oops, something went wrong.
585eadf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs: