Skip to content

Commit

Permalink
Merge pull request #19 from dnd-side-project/OZ-58-F-menu-page
Browse files Browse the repository at this point in the history
Feature : 메뉴 페이지 및 Modal 구현
  • Loading branch information
guesung committed Aug 20, 2023
2 parents ce6165a + 16449dd commit 8d607a0
Show file tree
Hide file tree
Showing 20 changed files with 334 additions and 39 deletions.
10 changes: 10 additions & 0 deletions public/icons/github.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions public/icons/instagram.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions public/images/menu/icon-button.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions src/app/(Sub)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { StrictPropsWithChildren } from '@/types';

export default function SubLayout({ children }: StrictPropsWithChildren) {
return (
<div>
<Spacing size={100} />
<>
<Spacing size={60} />
{children}
</div>
</>
);
}
26 changes: 26 additions & 0 deletions src/app/(Sub)/menu/components/InqueryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Button } from '@/components/Button';
import { Popup } from '@/components/Modal';
import { Spacing } from '@/components/Spacing';

interface MenuModalProps {
onConfirm?: () => void;
onClose: () => void;
}

export default function MenuModal({ onConfirm, onClose }: MenuModalProps) {
return (
<Popup>
<Spacing size={20} />
<div>문의사항을 남기시겠습니까?</div>
<Spacing size={20} />
<div className="flex w-full justify-evenly gap-8">
<Button className="bg-default" onClick={onClose}>
취소
</Button>
<Button className="bg-main-violet text-white" onClick={onConfirm}>
확인
</Button>
</div>
</Popup>
);
}
17 changes: 17 additions & 0 deletions src/app/(Sub)/menu/components/LoginSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { Spacing } from '@/components/Spacing';

export default function LoginSection() {
const handleLoginClick = () => {};

return (
<section className="py-12">
<div className="bg-violet flex items-center rounded-16 bg-main-violet-bright px-20 py-24">
<div className="h-40 w-40 rounded-full bg-black" />
<Spacing size={16} direction="horizontal" />
<p className="text-subtitle-1">dnd9_5_ozteam@kakao.com</p>
</div>
</section>
);
}
29 changes: 29 additions & 0 deletions src/app/(Sub)/menu/components/LogoutModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Button } from '@/components/Button';
import { Popup } from '@/components/Modal';
import { Spacing } from '@/components/Spacing';

interface MenuModalProps {
onConfirm?: () => void;
onClose: () => void;
}

export default function LogoutModal({ onConfirm, onClose }: MenuModalProps) {
return (
<Popup className="text-center">
<Spacing size={32} />
<h4>로그아웃</h4>
<Spacing size={8} />
<p>로그아웃시 북마크를 쓸 수 없어요.</p>
<p>정말 로그아웃하시겠어요?</p>
<Spacing size={32} />
<div className="flex w-full justify-evenly gap-8">
<Button className="bg-default" onClick={onConfirm}>
로그아웃
</Button>
<Button className="bg-main-violet text-white" onClick={onClose}>
로그인 유지
</Button>
</div>
</Popup>
);
}
25 changes: 25 additions & 0 deletions src/app/(Sub)/menu/components/MakerSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import BottomFixedDiv from '@/components/BottomFixedDiv';
import { Spacing } from '@/components/Spacing';
import Image from 'next/image';
import Link from 'next/link';

export default function MakerSection() {
return (
<BottomFixedDiv>
<div className="flex justify-center">
<Link href="https://www.instagram.com">
<Image alt="instagram" src="/icons/instagram.svg" width={48} height={48} />
</Link>
<Spacing size={16} direction="horizontal" />
<Link href="https://github.com/dnd-side-project/dnd-9th-5-frontend">
<Image alt="github" src="/icons/github.svg" width={48} height={48} />
</Link>
</div>
<Spacing size={8} />
<div className="flex justify-center">
<p className="text-12 text-caption">© POSEPICKER</p>
</div>
<Spacing size={60} />
</BottomFixedDiv>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Image from 'next/image';
export default function MenuHeader() {
const LeftNode = (
<div className="flex">
<Image width={24} height={24} src="/icons/arrow_back.svg" alt="back" />
<Image width={24} height={24} src="/icons/close.svg" alt="back" />
<Spacing size={12} direction="horizontal" />
<h4>메뉴</h4>
</div>
Expand Down
17 changes: 0 additions & 17 deletions src/app/(Sub)/menu/components/MenuModal.tsx

This file was deleted.

27 changes: 27 additions & 0 deletions src/app/(Sub)/menu/components/MenuSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client';

import { useOverlay } from '@/components/Overlay/useOverlay';
import MenuModal from './InqueryModal';
import LogoutModal from './LogoutModal';

export default function MenuListSection() {
const { open } = useOverlay();
const handleInquiryClick = () => {
open(({ exit }) => <MenuModal onClose={exit} />);
};

const handleLogoutClick = () => {
open(({ exit }) => <LogoutModal onClose={exit} />);
};

return (
<section className="flex flex-col">
<div className="flex flex-col py-12" onClick={handleInquiryClick}>
서비스 이용 문의
</div>
<div className="flex flex-col py-12 text-tertiary" onClick={handleLogoutClick}>
로그아웃
</div>
</section>
);
}
14 changes: 10 additions & 4 deletions src/app/(Sub)/menu/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import MenuHeader from './components/MenuHeader';
import { Spacing } from '@/components/Spacing';
import LoginSection from './components/LoginSection';
import MakerSection from './components/MakerSection';
import MenuHeader from './components/MenuListSection';
import MenuListSection from './components/MenuSection';

export default function MenuPage() {
return (
<>
<div className="h-full px-20">
<MenuHeader />
메뉴
</>
<LoginSection />
<MenuListSection />
<MakerSection />
</div>
);
}
11 changes: 6 additions & 5 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import '../../styles/font.css';
import '../../styles/typography.css';

import type { Metadata } from 'next';
import { OverlayProvider } from '@/components/Overlay/OverlayProvider';

import QueryProvider from './QueryProvider';

Expand Down Expand Up @@ -49,11 +50,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
<html lang="ko">
<body className="flex h-[100dvh] w-screen touch-none justify-center bg-slate-100 py-px">
<div className="h-full w-full max-w-440 bg-white text-black drop-shadow-2xl">
<QueryProvider>
{children}
<div id="portal" />
</QueryProvider>
<div className="h-full w-full max-w-440 bg-white text-primary drop-shadow-2xl">
<QueryProvider>
<OverlayProvider>{children}</OverlayProvider>
</QueryProvider>
<div id="portal" />
</div>
</body>
</html>
Expand Down
1 change: 0 additions & 1 deletion src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { ButtonHTMLAttributes, HTMLAttributes, PropsWithChildren } from 're

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
className?: string;
props?: PropsWithChildren<HTMLAttributes<HTMLButtonElement>>;
type?: 'button' | 'submit';
}

Expand Down
3 changes: 0 additions & 3 deletions src/components/Modal/ModalWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,17 @@ import { useOnClickOutside } from '@/hooks/useOnClickOutside';
import type { StrictPropsWithChildren } from '@/types';

interface ModalWrapperProps {
isOpen: boolean;
onClose?: () => void;
}

export default function ModalWrapper({
isOpen,
onClose = () => {},
children,
}: StrictPropsWithChildren<ModalWrapperProps>) {
const modalRef = useRef<HTMLDivElement>(null);

useOnClickOutside(modalRef, onClose);

if (!isOpen) return null;
return (
<AnimatedPortal
motionProps={{ initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }}
Expand Down
11 changes: 6 additions & 5 deletions src/components/Modal/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { StrictPropsWithChildren } from '@/types';
import ModalWrapper from './ModalWrapper';

interface ModalProps extends StrictPropsWithChildren {
isOpen: boolean;
interface PopupProps {
className?: string;
}
export default function Popup({ isOpen, children }: ModalProps) {

export default function Popup({ children }: StrictPropsWithChildren<PopupProps>) {
return (
<ModalWrapper isOpen={isOpen}>
<section className="flex w-280 flex-col items-center rounded-16 bg-white px-16 py-12">
<ModalWrapper>
<section className="flex w-300 flex-col items-center rounded-16 bg-white px-16 py-12">
{children}
</section>
</ModalWrapper>
Expand Down
38 changes: 38 additions & 0 deletions src/components/Overlay/OverlayController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/** @tossdocs-ignore */
import { Ref, useImperativeHandle, forwardRef, useEffect, useState, useCallback } from 'react';

import { CreateOverlayElement } from './type';

interface Props {
overlayElement: CreateOverlayElement;
onExit: () => void;
}

export interface OverlayControlRef {
close: () => void;
}

export const OverlayController = forwardRef(function OverlayController(
{ overlayElement: OverlayElement, onExit }: Props,
ref: Ref<OverlayControlRef>
) {
const [isOpen, setIsOpen] = useState(false);

const handleClose = useCallback(() => setIsOpen(false), []);

useImperativeHandle(
ref,
() => {
return { close: handleClose };
},
[handleClose]
);

useEffect(() => {
requestAnimationFrame(() => {
setIsOpen(true);
});
}, []);

return <OverlayElement isOpen={isOpen} close={handleClose} exit={onExit} />;
});
49 changes: 49 additions & 0 deletions src/components/Overlay/OverlayProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import React, {
createContext,
PropsWithChildren,
ReactNode,
useCallback,
useMemo,
useState,
} from 'react';

export const OverlayContext = createContext<{
mount(id: string, element: ReactNode): void;
unmount(id: string): void;
} | null>(null);
if (process.env.NODE_ENV !== 'production') {
OverlayContext.displayName = 'OverlayContext';
}

export function OverlayProvider({ children }: PropsWithChildren) {
const [overlayById, setOverlayById] = useState<Map<string, ReactNode>>(new Map());

const mount = useCallback((id: string, element: ReactNode) => {
setOverlayById((overlayById) => {
const cloned = new Map(overlayById);
cloned.set(id, element);
return cloned;
});
}, []);

const unmount = useCallback((id: string) => {
setOverlayById((overlayById) => {
const cloned = new Map(overlayById);
cloned.delete(id);
return cloned;
});
}, []);

const context = useMemo(() => ({ mount, unmount }), [mount, unmount]);

return (
<OverlayContext.Provider value={context}>
{children}
{[...overlayById.entries()].map(([id, element]) => (
<React.Fragment key={id}>{element}</React.Fragment>
))}
</OverlayContext.Provider>
);
}
6 changes: 6 additions & 0 deletions src/components/Overlay/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @tossdocs-ignore */
export type CreateOverlayElement = (props: {
isOpen: boolean;
close: () => void;
exit: () => void;
}) => JSX.Element;
Loading

0 comments on commit 8d607a0

Please sign in to comment.