Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: 사진목록 마크업 #65

Closed
wants to merge 13 commits into from
Prev Previous commit
Next Next commit
Merge branch 'develop' into feature/login
  • Loading branch information
yeynii committed Nov 4, 2023
commit 2a31ebbae183597faa267e94e05f6ff6f7614055
8 changes: 8 additions & 0 deletions .storybook/preview.ts → .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import React from 'react';

import { withThemeByDataAttribute } from '@storybook/addon-styling';

import { ModalProvider } from '../src/contexts/ModalProvider';
import '../src/global.css';

export const decorators = [
(Story: React.FC) => (
<ModalProvider>
<Story />
</ModalProvider>
),
withThemeByDataAttribute({
themes: {
light: 'light',
Expand Down
6 changes: 5 additions & 1 deletion src/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';

import { ModalProvider } from '@/contexts/ModalProvider';

import App from './App';
import './global.css';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<BrowserRouter>
<App />
<ModalProvider>
<App />
</ModalProvider>
</BrowserRouter>
</React.StrictMode>
);
4 changes: 4 additions & 0 deletions src/assets/avatar-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/x-button.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions src/contexts/ModalProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { createContext, type PropsWithChildren, useContext, useState } from 'react';

import { type ButtonProps } from '@/domain/_common/components';
import Modal from '@/domain/_common/components/Modal';

interface ModalData {
title: string;
message: string;
closeButton?: {
text: string;
} & Omit<ButtonProps, 'children'>;
confirmButton?: {
text: string;
} & Omit<ButtonProps, 'children'>;
}

interface ModalContextType {
openModal: (data: ModalData) => void;
closeModal: () => void;
}

const ModalContext = createContext<ModalContextType | undefined>(undefined);
export function ModalProvider({ children }: PropsWithChildren) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [modalData, setModalData] = useState<ModalData>({ title: '', message: '' });

const openModal = ({ title, message, closeButton, confirmButton }: ModalData) => {
setModalData({
title,
message,
closeButton,
confirmButton,
});
setIsOpen(true);
};

const closeModal = () => {
setIsOpen(false);
setModalData({ title: '', message: '' });
};

return (
<ModalContext.Provider value={{ openModal, closeModal }}>
{children}
{isOpen && (
<Modal
title={modalData.title}
message={modalData.message}
onClose={closeModal}
closeButton={modalData.closeButton}
confirmButton={modalData.confirmButton}
/>
)}
</ModalContext.Provider>
);
}

export function useModal() {
const context = useContext(ModalContext);
if (!context) {
throw new Error('useModal must be used within a ModalProvider');
}
return context;
}
63 changes: 63 additions & 0 deletions src/domain/_common/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ReactComponent as AvatarIcon } from '@/assets/avatar-icon.svg';
import { ReactComponent as DeleteButton } from '@/assets/x-button.svg';
import { generateVividColor } from '@/utils';

export interface AvatarProfilePhotoProps {
imageUrl?: string;
size?: 'small' | 'large';
}

export interface AvatarListProps extends AvatarProfilePhotoProps {
username: string;
}

export interface AvatarRowProps extends AvatarListProps {
onClickDelete: () => null;
}

function ProfilePhoto({ imageUrl, size = 'large' }: AvatarProfilePhotoProps) {
const backgroundColor = generateVividColor();

return (
<div
style={{ backgroundColor }}
className={`flex justify-center items-center ${size === 'small' ? 'h-5' : 'h-[34px]'} ${
size === 'small' ? 'w-5' : 'w-[34px]'
}
rounded-full overflow-hidden`}
>
{imageUrl !== undefined ? (
<div className={'w-full h-full bg-cover bg-center'} style={{ backgroundImage: `url(${imageUrl})` }} />
) : (
<div className={'w-1/2 h-1/2 m-auto'}>
<AvatarIcon />
</div>
)}
</div>
);
}

function ListRow({ imageUrl, username, onClickDelete }: AvatarRowProps) {
return (
<div>
<div className={'relative'}>
<ProfilePhoto imageUrl={imageUrl} />
<button type={'button'} onClick={onClickDelete} className={'absolute top-[-1px] right-[-1px]'}>
<DeleteButton />
</button>
</div>
<p className={'text-center text-grey-placeholder text-body8 mt-1'}>{username}</p>
</div>
);
}

function ListColumn({ imageUrl, username }: AvatarListProps) {
return (
<div className="text-center flex items-center text-[14px] font-medium gap-2.5">
<ProfilePhoto imageUrl={imageUrl} />
<p>{username}</p>
</div>
);
}

export const Avatar = { ProfilePhoto, ListRow, ListColumn };
28 changes: 19 additions & 9 deletions src/domain/_common/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
import React, { type ReactNode } from 'react';

interface ButtonProps {
export interface ButtonProps {
disabled?: boolean;
variant: 'normal' | 'text' | 'primary' | 'large';
variant?: 'text' | 'contained';
color?: 'grey' | 'primary' | 'red';
type?: 'button' | 'submit';
onClick?: () => void;
children?: ReactNode;
}

const buttonStyle = {
text: 'text-grey-buttontext active:bg-grey-background disabled:text-grey-whitegray disabled:bg-opacity-0',
normal:
'text-grey-buttontext bg-grey-background active:bg-grey-placeholder disabled:text-grey-whitegray disabled:bg-grey-background',
primary: 'text-white bg-primary-default active:bg-primary-touch disabled:bg-primary-disabled',
large: 'text-white bg-red-default w-full min-w-[336px] h-11 active:bg-red-touch disabled:bg-red-disabled',
grey: {
text: 'text-grey-buttontext active:bg-grey-buttontext active:bg-opacity-10 disabled:text-grey-placeholder disabled:bg-transparent',
contained:
'text-grey-buttontext bg-grey-background active:bg-grey-placeholder disabled:text-grey-placeholder disabled:bg-grey-whitegray',
},
primary: {
text: 'text-primary-default active:bg-primary-default active:bg-opacity-10 disabled:text-primary-disabled disabled:bg-transparent',
contained: 'text-white bg-primary-default active:bg-primary-touch disabled:bg-primary-disabled',
},
red: {
text: 'text-red-default active:bg-red-default active:bg-opacity-10 disabled:text-red-disabled disabled:bg-transparent',
contained: 'text-white bg-red-default active:bg-red-touch disabled:bg-red-disabled',
},
};

export function Button({
variant,
disabled = false,
variant = 'text',
color = 'grey',
onClick,
children,
type = 'submit',
}: React.PropsWithChildren<ButtonProps>) {
return (
<button
className={`border-none rounded-lg text-sm font-bold w-40 h-11 ${buttonStyle[variant]}`}
className={`border-none rounded-lg text-sm font-bold flex-1 p-[13px] ${buttonStyle[color][variant]}`}
disabled={disabled}
type={type}
onClick={onClick}
Expand Down
43 changes: 43 additions & 0 deletions src/domain/_common/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ReactComponent as CloseIcon } from '@/assets/close.svg';
import { Button, type ButtonProps } from '@/domain/_common/components/Button';

interface ModalProps {
title: string;
message: string;
onClose: () => void;
closeButton?: {
text: string;
} & Omit<ButtonProps, 'children'>;
confirmButton?: {
text: string;
} & Omit<ButtonProps, 'children'>;
}
export default function Modal({ title, message, onClose, closeButton, confirmButton }: ModalProps) {
return (
<>
<div className="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40" onClick={onClose} />
<div className="absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%] p-6 w-full">
<div className="z-10 m-auto max-w-[335px] bg-white rounded-[12px] text-center flex flex-col p-6 justify-center items-start gap-6">
<div className="text-[24px] font-bold stroke-grey-placeholder flex justify-between w-full">
{title} <CloseIcon className="cursor-pointer" onClick={onClose} />
</div>
<div className="text-[18px] font-bold text-grey-placeholder">{message}</div>
{(!!closeButton || !!confirmButton) && (
<div className="flex w-full gap-6">
{closeButton && (
<Button variant="contained" color="grey" onClick={onClose} {...closeButton}>
{closeButton.text}
</Button>
)}
{confirmButton && (
<Button variant="contained" color="red" {...confirmButton}>
{confirmButton.text}
</Button>
)}
</div>
)}
</div>
</div>
</>
);
}
5 changes: 3 additions & 2 deletions src/domain/_common/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export * from './Bubble';
export * from './Avatar';
export * from './NoticeBubble';
export * from './Button';
export * from './TextInput';
export * from './FloatingButton';
export * from './TextInput';
export * from './Checkbox';
export * from './Stepper';
export * from './IconButton';
Expand Down
2 changes: 1 addition & 1 deletion src/domain/_common/layouts/DefaultMobileLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function DefaultMobileLayout() {
return (
<main
className={
'm-auto grid min-h-screen w-full max-w-full grid-rows-header-footer bg-grey-background text-grey-300 sm:max-w-lg'
'm-auto grid min-h-screen w-full max-w-full grid-rows-header-footer bg-white text-grey-300 sm:max-w-lg px-[27px] py-6'
}
>
<Outlet />
Expand Down
1 change: 1 addition & 0 deletions src/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
min-width: 350px;
position: relative;
height: 100dvh;
background: #F6F6F6;
}
}
92 changes: 92 additions & 0 deletions src/stories/Avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { type Meta, type StoryFn } from '@storybook/react';
import React from 'react';

import {
Avatar,
type AvatarListProps,
type AvatarProfilePhotoProps,
type AvatarRowProps,
} from '@/domain/_common/components';
import { type Component } from '@storybook/blocks';

export default {
title: 'Components/Avatar',
component: Avatar.ProfilePhoto as Component,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
imageUrl: { control: 'text' },
},
} satisfies Meta;

const TemplateProfilePhoto: StoryFn<AvatarProfilePhotoProps> = (args) => <Avatar.ProfilePhoto {...args} />;
export const DefaultProfilePhoto = TemplateProfilePhoto.bind({});
DefaultProfilePhoto.argTypes = {
imageUrl: { control: 'text' },
size: { control: 'radio', options: ['large', 'small'] },
};

const TemplateListColumn: StoryFn<AvatarListProps> = (args) => <Avatar.ListColumn {...args} />;
export const DefaultListColumn = TemplateListColumn.bind({});
DefaultListColumn.args = {
username: 'test',
};

const TemplateListRow: StoryFn<AvatarRowProps> = (args) => <Avatar.ListRow {...args} />;
export const DefaultListRow = TemplateListRow.bind({});
DefaultListRow.args = {
username: 'test',
onClickDelete: () => null,
};

export const SmallAvatar = TemplateProfilePhoto.bind({});
SmallAvatar.args = {
size: 'small',
};

export const ProfileImgAvatar = TemplateProfilePhoto.bind({});
ProfileImgAvatar.args = {
imageUrl: 'https://cdn.eyesmag.com/content/uploads/posts/2023/06/20/new-ec23e638-7918-4f8a-96cf-f43ce26826e2.jpg',
};

export const SmallProfileImgAvatar = TemplateProfilePhoto.bind({});
SmallProfileImgAvatar.args = {
size: 'small',
imageUrl: 'https://cdn.eyesmag.com/content/uploads/posts/2023/06/20/new-ec23e638-7918-4f8a-96cf-f43ce26826e2.jpg',
};

export const AvatarListColumnSet = () => {
const users = [
{ username: '시운', imageUrl: 'https://avatars.githubusercontent.com/u/78866590?v=4' },
{ username: '도훈', imageUrl: 'https://avatars.githubusercontent.com/u/65100540?v=4' },
{ username: '예윤' },
{ username: '상준' },
];

return (
<div className={'flex-col space-y-2.5'}>
{users.map((user, index) => (
<Avatar.ListColumn key={index} {...user} />
))}
</div>
);
};

export const AvatarListRowSet = () => {
const users = [
{ username: '시운', imageUrl: 'https://avatars.githubusercontent.com/u/78866590?v=4' },
{ username: '도훈', imageUrl: 'https://avatars.githubusercontent.com/u/65100540?v=4' },
{ username: '예윤' },
{ username: '상준' },
];

return (
<div className={'flex gap-2.5'}>
{users.map((user, index) => (
<Avatar.ListRow key={index} {...user} onClickDelete={() => null} />
))}
</div>
);
};
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.