Skip to content

Commit

Permalink
feature: 유저 페이지 모달 연결 및 툴팁 컴포넌트 구현 (#32)
Browse files Browse the repository at this point in the history
* feat: blue color 추가

* design: Dimmed z-index 부여

* feat: AddMenu 컴포넌트 구현

* design: Modal z-index 부여

* refactor: hook export

* feat: useModals hook 추가

여러 모달들의 state를 관리

* feat: FilmAddModal 추가

* feat: 필름제목수정 모달 추가

* feat: useSafeContext hook 추가

* feat: arrow down svg 추가

* feat: Select 컴포넌트 구현

* design: Select item 커서 포인터 추가

* feat: FilmSelectModal 추가

* feat: 유저 페이지 모달 연결

* feat: pencil 아이콘 추가

* refactor: hook 이름 수정

* chore: eslint rule 수정

* feat: ModalProvider 생성

* feat: 유저페이지 ModalProvider wrapping

* feat: Drawer 회원가입, 로그인 링킹

* feat: 필름 제목 edit 모달 연결

* design: modal min height 설정

* design: input full width 추가

* fix: camera svg size를 조절할 수 없던 문제 해결

* feat: 유저페이지 프로필모달 연결

* feat: isString type util 추가

* feat: Close 아이콘 추가

* feat: tooltip 컴포넌트 추가

* feat: 유저페이지 tooltip 컴포넌트 적용

* feat: add페이지 title query 적용
  • Loading branch information
asdf99245 authored Aug 12, 2023
1 parent cb2a7c9 commit 367195e
Show file tree
Hide file tree
Showing 33 changed files with 599 additions and 68 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-types": "off"
}
}
3 changes: 3 additions & 0 deletions src/assets/icons/arrow-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/icons/camera.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ export { default as LeftChevron } from './left-chevron.svg';
export { default as RightChevronBG } from './right-chevron-bg.svg';
export { default as LeftChevronBG } from './left-chevron-bg.svg';
export { default as Camera } from './camera.svg';
export { default as ArrowDown } from './arrow-down.svg';
export { default as Pencil } from './pencil.svg';
export { default as Close } from './close.svg';
4 changes: 4 additions & 0 deletions src/assets/icons/pencil.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 14 additions & 19 deletions src/components/shared/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import Image from 'next/image';
import type { ComponentProps, MouseEventHandler } from 'react';
import { Profile } from '@/pages/user/[id]';
import clsx from 'clsx';
import { cn } from '@/utils/cn';

const PLACEHOLDER_SRC = '/images/avatar-placeholder.png';
const HINT_TEXT = '한줄소개를 작성해주세요';
const SIZE = 80;

interface AvatarProps {
nickname: string;
src?: string;
displayMeta?: boolean;
description?: string;
viewCount?: number;
onClick?: MouseEventHandler<HTMLImageElement>;
onEditProfile?: (info: Profile) => void;
}

type Props = AvatarProps &
Expand All @@ -23,12 +23,16 @@ export function Avatar({
src = PLACEHOLDER_SRC,
nickname,
displayMeta = false,
description,
description = '',
viewCount = 0,
className,
onClick,
onEditProfile,
...restProps
}: Props) {
const handleEditProfile: MouseEventHandler<HTMLElement> = () => {
if (onEditProfile) onEditProfile({ profileImage: src, nickname, description });
};

return (
<div
className={cn(
Expand All @@ -38,30 +42,21 @@ export function Avatar({
>
<div
className='tw-relative tw-h-20 tw-w-20 tw-cursor-pointer tw-overflow-hidden tw-rounded-full'
onClick={onClick}
onClick={handleEditProfile}
>
<Image
src={src}
alt={nickname}
width={SIZE}
height={SIZE}
className='tw-object-cover tw-object-center'
{...restProps}
/>
<Image src={src} alt={nickname} fill className='tw-object-contain tw-object-center' {...restProps} />
</div>
{displayMeta && (
<div className='tw-flex tw-flex-col tw-gap-3'>
<div className='tw-flex tw-items-center tw-gap-2.5'>
<div className='tw-flex tw-items-center tw-gap-2.5' onClick={handleEditProfile}>
<strong className='tw-text-accent-eng'>{nickname}</strong>
<span className='tw-text-caption-eng tw-text-grayscale-500'>{`Total ${viewCount}`}</span>
</div>
<p
className={clsx(
'tw-text-body2-accent',
!description && 'tw-text-grayscale-300',
)}
className={clsx('tw-text-body2-accent', !description && 'tw-text-grayscale-300')}
onClick={handleEditProfile}
>
{description ?? HINT_TEXT}
{description.length === 0 && HINT_TEXT}
</p>
</div>
)}
Expand Down
5 changes: 1 addition & 4 deletions src/components/shared/Dimmed/Dimmed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ type Props = HTMLAttributes<HTMLDivElement>;

export function Dimmed(props: Props) {
return (
<div
className='tw-fixed tw-left-0 tw-top-0 tw-h-full tw-w-full tw-bg-black tw-opacity-60'
{...props}
/>
<div className='tw-fixed tw-left-0 tw-top-0 tw-z-10 tw-h-full tw-w-full tw-bg-black tw-opacity-60' {...props} />
);
}
6 changes: 4 additions & 2 deletions src/components/shared/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Fragment, type MouseEventHandler, useEffect, useState } from 'react';
import { Button, Dimmed, Icon } from '@/components/shared';
import { cn } from '@/utils/cn';
Expand All @@ -13,6 +14,7 @@ interface Props {

export function Drawer({ isOpen, onClose }: Props) {
const [mounted, setMounted] = useState(false);
const router = useRouter();

useEffect(() => {
if (isOpen) {
Expand Down Expand Up @@ -48,12 +50,12 @@ export function Drawer({ isOpen, onClose }: Props) {
나의 취향을 전시할 수 있는 <br />
바이오그래피, 그라피입니다
</p>
<Button variant='link' className='tw-ml-auto tw-h-12 tw-w-[218px]'>
<Button variant='link' className='tw-ml-auto tw-h-12 tw-w-[218px]' onClick={() => router.push('/signup')}>
회원가입하기 <Icon iconType='RightArrow' />
</Button>
<ul className='tw-text-body1 tw-ml-auto tw-mt-9 tw-w-[218px] [&>li]:tw-border-b [&>li]:tw-border-black [&>li]:tw-py-2.5 [&>li]:tw-pl-[22px]'>
<li>
<Link href='/'>로그인하기</Link>
<Link href='/signin'>로그인하기</Link>
</li>
<li>
<Link href='/'>의견 보내기</Link>
Expand Down
2 changes: 1 addition & 1 deletion src/components/shared/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const Input = forwardRef<HTMLInputElement, Props>(
};

return (
<div className='tw-flex tw-flex-col tw-text-black'>
<div className='tw-flex tw-w-full tw-flex-col tw-text-black'>
{label && (
<div className='tw-mb-3 tw-flex tw-w-full tw-items-center'>
<label
Expand Down
2 changes: 1 addition & 1 deletion src/components/shared/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function Modal({
return (
<Portal>
<Dimmed onClick={onCancel} />
<div className='tw-translate tw-fixed tw-left-1/2 tw-top-1/2 tw-flex tw-h-[432px] tw-w-[336px] tw--translate-x-1/2 tw--translate-y-1/2 tw-flex-col tw-items-center tw-rounded tw-bg-white tw-px-5 tw-pb-6 tw-shadow-lg'>
<div className='tw-translate tw-fixed tw-left-1/2 tw-top-1/2 tw-z-10 tw-flex tw-min-h-[432px] tw-w-[336px] tw--translate-x-1/2 tw--translate-y-1/2 tw-flex-col tw-items-center tw-rounded tw-bg-white tw-px-5 tw-pb-6 tw-shadow-lg'>
<div className='tw-text-main-headline tw-h-[88px] tw-pb-[30px] tw-pt-[38px]'>
<h2>{title}</h2>
</div>
Expand Down
82 changes: 82 additions & 0 deletions src/components/shared/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { MouseEventHandler, PropsWithChildren } from 'react';
import { createContext, useState } from 'react';
import { useSafeContext } from '@/hooks';
import { Icon } from '@/components/shared/Icon';
import { cn } from '@/utils/cn';

interface SelectProps {
isExpanded?: boolean;
selected: string;
onSelect: (option: string) => void;
}

interface SelectItemProps {
children: string;
}

interface SelectContextType {
selected: string;
closeSelect: () => void;
onSelect: (option: string) => void;
}
const SelectContext = createContext<SelectContextType | null>(null);

export function Select({
children,
isExpanded: isExpandedFromProps = false,
selected,
onSelect,
}: PropsWithChildren<SelectProps>) {
const [isExpanded, setIsExpanded] = useState(isExpandedFromProps);

const handleToggleSelect: MouseEventHandler<SVGSVGElement> = () => {
setIsExpanded((prev) => !prev);
};

const closeSelect = () => {
setIsExpanded(false);
};

return (
<div className='rounded tw-flex tw-w-full tw-flex-col tw-overflow-hidden tw-border tw-border-grayscale-300'>
<div className='tw-flex tw-h-12 tw-w-full tw-items-center tw-justify-between tw-pl-2.5 tw-pr-3'>
<span className='tw-text-body2-accent tw-text-grayscale-700'>{selected}</span>
<Icon
iconType='ArrowDown'
className={cn('tw-top-1/2 tw-cursor-pointer', isExpanded && '-tw-rotate-180')}
onClick={handleToggleSelect}
/>
</div>
<div className={cn('tw-h-0 tw-w-full', isExpanded && 'tw-h-fit')}>
<ul className='tw-w-full'>
<SelectContext.Provider value={{ selected, closeSelect, onSelect }}>{children}</SelectContext.Provider>
</ul>
</div>
</div>
);
}

function SelectItem({ children }: SelectItemProps) {
const { selected, closeSelect, onSelect } = useSafeContext(SelectContext);

const handleSelect =
(option: string): MouseEventHandler<HTMLLIElement> =>
() => {
onSelect(option);
closeSelect();
};

return (
<li
className={cn(
'tw-flex-justify-between tw-text-body2 tw-flex tw-h-12 tw-w-full tw-cursor-pointer tw-items-center tw-border-t tw-border-grayscale-300 tw-bg-grayscale-100 tw-p-2.5 tw-text-grayscale-400',
selected === children && 'tw-hidden',
)}
onClick={handleSelect(children)}
>
{children}
</li>
);
}

Select.Item = SelectItem;
1 change: 1 addition & 0 deletions src/components/shared/Select/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Select';
27 changes: 27 additions & 0 deletions src/components/shared/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { type HTMLAttributes, type MouseEventHandler, type PropsWithChildren, useState } from 'react';
import { Icon } from '@/components/shared/Icon';

interface Props extends HTMLAttributes<HTMLDivElement> {
isOpen?: boolean;
text: string;
}

export function Tooltip({ children, isOpen: isOpenFromProps = true, text, ...restProps }: PropsWithChildren<Props>) {
const [isOpen, setIsOpen] = useState(isOpenFromProps);

const handleClickClose: MouseEventHandler<SVGSVGElement> = () => {
setIsOpen(false);
};

return (
<div className='tw-relative' {...restProps}>
{children}
{isOpen && (
<div className='tw-text-body2-accent tw-absolute tw-right-full tw-top-1/2 tw-flex tw-w-[200px] -tw-translate-x-1 -tw-translate-y-1/2 tw-items-center tw-justify-between tw-gap-1 tw-rounded tw-bg-nudge tw-px-2 tw-py-1 after:tw-absolute after:tw-right-0 after:tw-h-2.5 after:tw-w-2.5 after:tw-translate-x-1/2 after:tw-rotate-45 after:tw-bg-nudge after:tw-content-[""]'>
<p className='tw-shrink-0'>{text}</p>
<Icon iconType='Close' className='tw-cursor-pointer tw-fill-grayscale-700' onClick={handleClickClose} />
</div>
)}
</div>
);
}
1 change: 1 addition & 0 deletions src/components/shared/Tooltip/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Tooltip'
2 changes: 2 additions & 0 deletions src/components/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export * from './Portal';
export * from './Textarea';
export * from './ImageFrame';
export * from './TextButton';
export * from './Select';
export * from './Tooltip'
31 changes: 31 additions & 0 deletions src/components/user/AddMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { MouseEventHandler } from 'react';
import { Dimmed, Portal } from '@/components/shared';
import { cn } from '@/utils/cn';

interface Props {
isOpen: boolean;
onClose: MouseEventHandler<HTMLDivElement>;
onAddFilm: MouseEventHandler<HTMLDivElement>;
onUploadPhoto: MouseEventHandler<HTMLDivElement>;
}

const menuStyle =
'tw-w-full tw-cursor-pointer tw-py-[22px] tw-text-blue tw-flex tw-justify-center tw-items-center hover:tw-bg-grayscale-100';

export function AddMenu({ isOpen, onClose, onAddFilm, onUploadPhoto }: Props) {
if (!isOpen) return null;

return (
<Portal>
<Dimmed onClick={onClose} />
<div className='tw-fixed tw-left-1/2 tw-top-1/2 tw-z-10 tw-w-[335px] -tw-translate-x-1/2 -tw-translate-y-1/2 tw-overflow-hidden tw-rounded tw-bg-white'>
<div className={cn(menuStyle, 'tw-border-b')} onClick={onAddFilm}>
새로운 필름 추가
</div>
<div className={menuStyle} onClick={onUploadPhoto}>
포토컷 올리기
</div>
</div>
</Portal>
);
}
18 changes: 11 additions & 7 deletions src/components/user/CameraRoll.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import { type HTMLAttributes } from 'react';
import { Icon } from '@/components/shared';
import { cn } from '@/utils/cn';

const FILM_HOLE_COUNT = 11;

interface Props extends HTMLAttributes<HTMLDivElement> {
title: string;
photos?: string[];
onEditTitle: (title: string) => void;
}

export function CameraRoll({
title,
photos = [],
className,
...restProps
}: Props) {
export function CameraRoll({ title, photos = [], onEditTitle, className, ...restProps }: Props) {
const srcs = Array.from({ length: 10 }, (_, i) => photos[i] ?? '');

return (
Expand All @@ -22,7 +19,14 @@ export function CameraRoll({
{...restProps}
>
<div className='tw-flex tw-items-center tw-justify-between tw-py-2 tw-pl-3.5 tw-pr-5'>
<h2 className='tw-text-body1 tw-text-grayscale-200'>{title}</h2>
<div className='tw-flex tw-items-center tw-gap-1'>
<h2 className='tw-text-body1 tw-text-grayscale-200'>{title}</h2>
<Icon
iconType='Edit'
className='tw-cursor-pointer tw-fill-grayscale-400'
onClick={() => onEditTitle(title)}
/>
</div>
<span className='tw-text-caption-eng tw-text-grayscale-100'>{`${photos.length} Cuts`}</span>
</div>
<div className='tw-flex tw-gap-2.5 tw-overflow-x-scroll tw-scrollbar-hide'>
Expand Down
23 changes: 23 additions & 0 deletions src/components/user/FilmAddModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type ComponentProps, type MouseEventHandler, useState } from 'react';
import { Input, Modal } from '@/components/shared';

type Props = Omit<ComponentProps<typeof Modal>, 'title' | 'children'>;

export function FilmAddModal({ onCancel, ...restProps }: Props) {
const [input, setInput] = useState('');

const handleValueChange = (value: string) => {
setInput(value);
};

const handleSave: MouseEventHandler<HTMLButtonElement> = (e) => {
// TODO: 필름 추가 API 연결
if (onCancel) onCancel(e);
};

return (
<Modal title='새로운 필름 추가' onCancel={onCancel} onSave={handleSave} {...restProps}>
<Input label='필름 제목' placeholder='필름 제목을 입력하세요' value={input} onValueChange={handleValueChange} />
</Modal>
);
}
Loading

0 comments on commit 367195e

Please sign in to comment.