Skip to content

Commit

Permalink
feat(shared/ui): add image modal to avatar
Browse files Browse the repository at this point in the history
  • Loading branch information
ooooorobo committed Jul 14, 2024
1 parent 4f0c0ee commit a7a3377
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 32 deletions.
107 changes: 99 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"generate:api": "orval --config ./orval.config.cjs"
},
"dependencies": {
"@radix-ui/react-portal": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@remix-run/node": "^2.9.2",
"@remix-run/react": "^2.9.2",
Expand Down
45 changes: 24 additions & 21 deletions src/shared/ui/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DetailedHTMLProps, ImgHTMLAttributes, ReactNode } from 'react';
import styles from './Avatar.module.css';
import { useImageLoadingStatus } from 'src/shared/functions/useImageLoadingStatus';

type AvatarProp = DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> & {
export type AvatarProp = DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> & {
fallback: ReactNode;
shape: 'circle' | 'roundedSquare';
size: number;
Expand All @@ -14,25 +14,28 @@ export const Avatar = ({ className = '', actionSlot, shape, size, alt, fallback,
const loadingStatus = useImageLoadingStatus(props.src);

return (
<div
className={`${styles.Container} ${className}`}
role={onClick ? 'button' : undefined}
style={{ '--size': size }}
data-shape={shape}
data-loading={loadingStatus === 'loading'}
onClick={onClick}
onKeyDown={onClick}
>
{loadingStatus === 'success' && (
<img className={styles.Image} alt={alt ?? '이미지'} data-loading={false} {...props} />
)}
{loadingStatus === 'error' && <span className={styles.Fallback}>{fallback}</span>}
{actionSlot && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<span className={styles.ActionSlot} onClick={(e) => e.stopPropagation()}>
{actionSlot}
</span>
)}
</div>
<>
<div
className={`${styles.Container} ${className}`}
role={onClick ? 'button' : undefined}
style={{ '--size': size }}
data-shape={shape}
data-loading={loadingStatus === 'loading'}
draggable={false}
onClick={onClick}
onKeyDown={onClick}
>
{loadingStatus === 'success' && (
<img className={styles.Image} alt={alt ?? '이미지'} data-loading={false} {...props} />
)}
{loadingStatus === 'error' && <span className={styles.Fallback}>{fallback}</span>}
{actionSlot && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions
<span className={styles.ActionSlot} onClick={(e) => e.stopPropagation()}>
{actionSlot}
</span>
)}
</div>
</>
);
};
19 changes: 19 additions & 0 deletions src/shared/ui/AvatarList/AvatarList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Meta, StoryObj } from '@storybook/react';
import { AvatarList } from 'src/shared/ui/AvatarList/AvatarList';
import { useState } from 'react';

const meta: Meta<typeof AvatarList> = {
component: AvatarList,
};

export default meta;
type Story = StoryObj<typeof AvatarList>;

function AvatarListStory() {
const [files, setFiles] = useState<File[]>([]);
return <AvatarList files={files} setFiles={setFiles} />;
}

export const Default: Story = {
render: AvatarListStory,
};
18 changes: 15 additions & 3 deletions src/shared/ui/AvatarList/AvatarList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,40 @@ import { Avatar } from 'src/shared/ui/Avatar/Avatar';
import { Close, Plus } from 'src/shared/ui/icons';
import styles from './AvatarList.module.css';
import { useDataUrlListFromFiles } from 'src/shared/functions/useDataUrlListFromFiles';
import { AvatarWithModal } from 'src/shared/ui/AvatarWithModal/AvatarWithModal';

type Props = {
files: File[];
setFiles?: (getState: (prevFiles: File[]) => File[]) => void;
};

export const AvatarList = ({ files, setFiles }: Props) => {
export const AvatarList = ({ files = [], setFiles }: Props) => {
const dataUrlList = useDataUrlListFromFiles(files);

const onFileChanged = (files: File[]) => {
setFiles?.((prev) => [...prev, ...files]);
};

const onClickRemove = (targetIdx: number) => {
setFiles?.((prev) => prev.filter((_, idx) => idx !== targetIdx));
};

return (
<div className={styles.ImageContainer}>
{setFiles && (
<UploadTrigger onUploadFiles={onFileChanged} accept={'image/*'} multiple>
{(onClickUpload) => <Avatar fallback={<Plus />} shape={'roundedSquare'} size={72} onClick={onClickUpload} />}
</UploadTrigger>
)}
{dataUrlList.map((url) => (
<Avatar key={url} fallback={''} shape={'roundedSquare'} size={72} src={url} actionSlot={<Close />} />
{dataUrlList.map((url, idx) => (
<AvatarWithModal
key={url}
fallback={''}
shape={'roundedSquare'}
size={72}
src={url}
actionSlot={<Close onClick={() => onClickRemove(idx)} />}
/>
))}
</div>
);
Expand Down
16 changes: 16 additions & 0 deletions src/shared/ui/AvatarWithModal/AvatarWithModal.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Meta, StoryObj } from '@storybook/react';
import { AvatarWithModal } from 'src/shared/ui/AvatarWithModal/AvatarWithModal';

const meta: Meta<typeof AvatarWithModal> = {
component: AvatarWithModal,
};

export default meta;
type Story = StoryObj<typeof AvatarWithModal>;

export const Default: Story = {
args: {
src: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/20230905_Haerin_%28NewJeans%29.jpg/250px-20230905_Haerin_%28NewJeans%29.jpg',
size: 72,
},
};
28 changes: 28 additions & 0 deletions src/shared/ui/AvatarWithModal/AvatarWithModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Avatar, AvatarProp } from 'src/shared/ui/Avatar/Avatar';
import { useState } from 'react';
import { ImageModal } from 'src/shared/ui/ImageModal/ImageModal';

type AvatarWithModalProps = AvatarProp;

export const AvatarWithModal = ({ fallback, shape, size, actionSlot, onClick, ...props }: AvatarWithModalProps) => {
const [showModal, setShowModal] = useState(false);

const handleClickAvatar = () => {
onClick?.();
setShowModal(true);
};

return (
<>
<Avatar
fallback={fallback}
shape={shape}
size={size}
actionSlot={actionSlot}
{...props}
onClick={handleClickAvatar}
/>
<ImageModal showModal={showModal} closeModal={() => setShowModal(false)} {...props} />
</>
);
};
44 changes: 44 additions & 0 deletions src/shared/ui/ImageModal/ImageModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.ImageModal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100%;
}

.ImageModalDim {
width: 100%;
height: 100%;
background-color: #00000033;
}

.ImageModalWrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
padding: 20px;

pointer-events: none;

display: flex;
justify-content: center;
align-items: center;
}

.ImageModalImg {
object-fit: contain;
width: max-content;
height: max-content;
max-width: 100%;
max-height: 100%;
pointer-events: bounding-box;
}

.ImageModalCloseButton {
position: absolute;
top: 0;
right: 0;
}
18 changes: 18 additions & 0 deletions src/shared/ui/ImageModal/ImageModal.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Meta, StoryObj } from '@storybook/react';
import { ImageModal } from 'src/shared/ui/ImageModal/ImageModal';

const meta: Meta<typeof ImageModal> = {
component: ImageModal,
};

export default meta;
type Story = StoryObj<typeof ImageModal>;

export const Default: Story = {
args: {
showModal: true,
closeModal: () => alert('close'),
closeOnClickOutside: true,
src: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/20230905_Haerin_%28NewJeans%29.jpg/250px-20230905_Haerin_%28NewJeans%29.jpg',
},
};
Loading

0 comments on commit a7a3377

Please sign in to comment.