diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html
index 24fbaed..5aa0018 100644
--- a/.storybook/preview-body.html
+++ b/.storybook/preview-body.html
@@ -1,2 +1,3 @@
+
\ No newline at end of file
diff --git a/package.json b/package.json
index a89564b..7a82f72 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
},
"devDependencies": {
"@babel/core": "^7.17.7",
+ "@babel/runtime": "^7.12.8",
"@emotion/babel-plugin": "^11.7.2",
"@storybook/addon-a11y": "^6.4.19",
"@storybook/addon-actions": "^6.4.19",
diff --git a/src/components/Auth/Auth.styled.tsx b/src/components/Auth/Auth.styled.tsx
index 833ba72..dc61c28 100644
--- a/src/components/Auth/Auth.styled.tsx
+++ b/src/components/Auth/Auth.styled.tsx
@@ -2,6 +2,32 @@ import styled from '@emotion/styled';
import { pxToRem } from 'utils';
import { StyleInputProps } from './Auth.types';
+export const StyledAuthContainer = styled.div`
+ padding: 15vh 0 0 0;
+ > * {
+ max-width: ${pxToRem(400)};
+ display: block;
+ width: 40vw;
+ min-width: ${pxToRem(300)};
+ margin: 0 auto;
+ }
+`;
+
+export const StyledForm = styled.form`
+ margin: ${pxToRem(18)} auto;
+ min-width: ${pxToRem(280)};
+ display: flex;
+ flex-direction: column;
+ gap: ${pxToRem(4)};
+ input,
+ button {
+ padding: 0 ${pxToRem(24)};
+ height: ${pxToRem(36)};
+ border: none;
+ border-radius: ${pxToRem(5)} ${pxToRem(5)};
+ }
+`;
+
export const StyledFieldError = styled.div`
height: ${pxToRem(32)};
line-height: 1.8;
diff --git a/src/components/Auth/Auth.tsx b/src/components/Auth/Auth.tsx
index 682b691..3148cf2 100644
--- a/src/components/Auth/Auth.tsx
+++ b/src/components/Auth/Auth.tsx
@@ -1,35 +1,40 @@
import { FormikProps, withFormik } from 'formik';
import { FormValues, FormProps } from './Auth.types';
-import { StyledInput, StyledFieldError } from './Auth.styled';
+import { StyledForm, StyledInput, StyledFieldError, StyledAuthContainer } from './Auth.styled';
import { AUTH_FUNC, SCHEMA, INITIAL_VALUES, FIELDS, HEADING, PLACEHOLDER, TYPE } from './AuthServices';
+import { Button } from 'components/Button/Button';
const AuthForm = (props: FormProps & FormikProps): JSX.Element => {
const { currentForm, values, errors, dirty, touched, isValid, handleChange, handleBlur, handleSubmit } = props;
return (
-
+
+
+
);
};
diff --git a/src/components/Button/Button.styled.tsx b/src/components/Button/Button.styled.tsx
index 1ddf08d..b717eae 100644
--- a/src/components/Button/Button.styled.tsx
+++ b/src/components/Button/Button.styled.tsx
@@ -6,6 +6,10 @@ export const StyledButton = styled.button`
border: none;
background: none;
border-radius: ${({ round }) => round && '30px'};
+ &:disabled {
+ background-color: ${({ theme }) => theme.color.gray300};
+ cursor: not-allowed;
+ }
`;
export const StyledOutlineButton = styled(StyledButton)`
diff --git a/src/components/Button/Button.types.ts b/src/components/Button/Button.types.ts
index 9867044..7e47160 100644
--- a/src/components/Button/Button.types.ts
+++ b/src/components/Button/Button.types.ts
@@ -12,6 +12,7 @@ export interface StyledButtonProps {
className?: string;
onClick?: () => void;
title?: string;
+ disabled?: boolean;
}
export interface ButtonProps extends StyledButtonProps {
diff --git a/src/components/Dialog/Dialog.stories.tsx b/src/components/Dialog/Dialog.stories.tsx
new file mode 100644
index 0000000..0abbb48
--- /dev/null
+++ b/src/components/Dialog/Dialog.stories.tsx
@@ -0,0 +1,21 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Dialog } from './Dialog';
+
+export default {
+ title: 'Dialog',
+ component: Dialog,
+ args: {
+ onClose: () => console.log('closed'),
+ children: hahaha
,
+ nodeId: 'dialog',
+ label: 'test',
+ },
+} as ComponentMeta;
+
+const Template: ComponentStory = (args) => ;
+
+export const DefaultDialog = Template.bind({});
+
+export const TestDialog = Template.bind({});
+
+TestDialog.args = { ...DefaultDialog.args, children: TESTTEST
};
diff --git a/src/components/Dialog/Dialog.styled.tsx b/src/components/Dialog/Dialog.styled.tsx
new file mode 100644
index 0000000..50573c8
--- /dev/null
+++ b/src/components/Dialog/Dialog.styled.tsx
@@ -0,0 +1,46 @@
+import styled from '@emotion/styled';
+import { IconButton } from 'components';
+
+export const StyledDialogContainer = styled.div`
+ z-index: 200;
+ position: fixed;
+ top: 0;
+ width: 90%;
+ height: 90%;
+ overflow: auto;
+`;
+
+export const StyledDialogContent = styled.div`
+ z-index: 200;
+ color: #121212;
+ background: rgba(36, 36, 36, 0.8);
+ backdrop-filter: blur(3px);
+ min-height: 100%;
+`;
+
+export const StyledCloseButton = styled(IconButton)`
+ cursor: pointer;
+ position: absolute;
+ z-index: 200;
+ top: 20px;
+ right: 20px;
+ border: 0;
+ padding: 10px;
+ background: transparent;
+ color: #fefefe;
+ svg {
+ pointer-events: none;
+ fill: currentColor;
+ }
+`;
+
+export const StyledDim = styled.div`
+ position: absolute;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(36, 36, 36, 0.8);
+ backdrop-filter: blur(2px);
+`;
diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx
new file mode 100644
index 0000000..d953e31
--- /dev/null
+++ b/src/components/Dialog/Dialog.tsx
@@ -0,0 +1,79 @@
+import { useRef, useEffect, useCallback } from 'react';
+import { createPortal } from 'react-dom';
+import { getTabbableElements } from 'utils';
+import { StyledCloseButton, StyledDialogContainer, StyledDialogContent, StyledDim } from './Dialog.styled';
+import { DialogProps } from './Dialog.types';
+
+export function Dialog({ onClose, children, nodeId = 'dialog', label, ...restProps }: DialogProps) {
+ const dialogRef = useRef(null);
+ const openButtonRef = useRef(null);
+
+ const handleClose = useCallback(() => {
+ onClose();
+ openButtonRef.current.focus();
+ }, [onClose]);
+
+ useEffect(() => {
+ openButtonRef.current = document.activeElement;
+
+ const tabbableElements = getTabbableElements(dialogRef.current!);
+ const firstTabbableElement = tabbableElements[0];
+ const lastTabbableElement = tabbableElements[tabbableElements.length - 1];
+
+ firstTabbableElement.focus();
+ let eventType = 'keydown';
+
+ const eventListener = (e: KeyboardEvent) => {
+ const { key, shiftKey, target } = e;
+
+ if (Object.is(target, firstTabbableElement) && shiftKey && key === 'Tab') {
+ e.preventDefault();
+ lastTabbableElement.focus();
+ }
+
+ if (Object.is(target, lastTabbableElement) && !shiftKey && key === 'Tab') {
+ e.preventDefault();
+ firstTabbableElement.focus();
+ }
+
+ if (key === 'Escape') {
+ handleClose();
+ }
+ };
+ document.addEventListener(eventType as keyof DocumentEventMap, eventListener as EventListener);
+ document.body.style['overflow-y'] = 'hidden';
+ document.getElementById('__next')!.setAttribute('aria-hidden', 'true');
+
+ return () => {
+ document.removeEventListener(eventType, eventListener as EventListener);
+ document.getElementById('__next')!.removeAttribute('aria-hidden');
+ document.body.style['overflow-y'] = '';
+ };
+ }, [handleClose, label]);
+
+ return createPortal(
+ <>
+
+ {children}
+
+
+
+ >,
+ document.getElementById(nodeId),
+ );
+}
diff --git a/src/components/Dialog/Dialog.types.ts b/src/components/Dialog/Dialog.types.ts
new file mode 100644
index 0000000..f3acfe2
--- /dev/null
+++ b/src/components/Dialog/Dialog.types.ts
@@ -0,0 +1,9 @@
+import React from 'react';
+
+export interface DialogProps {
+ onClose: () => void;
+ children: React.ReactNode;
+ nodeId?: string;
+ label: string;
+ [restProps: string]: any;
+}
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
new file mode 100644
index 0000000..064d3aa
--- /dev/null
+++ b/src/pages/_document.tsx
@@ -0,0 +1,17 @@
+import { Html, Head, Main, NextScript } from 'next/document';
+import { useContext } from 'react';
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/search/[keyword].tsx b/src/pages/search/[keyword].tsx
new file mode 100644
index 0000000..5daedd2
--- /dev/null
+++ b/src/pages/search/[keyword].tsx
@@ -0,0 +1,47 @@
+import { useRouter } from 'next/router';
+import { NextPage } from 'next';
+import { useSearchRecipeQuery } from 'store/services';
+import { useState } from 'react';
+const RESULTS_PER_PAGE = 12;
+
+const Search: NextPage = ({ data }) => {
+ // const {
+ // query: { keyword },
+ // } = useRouter();
+ // const [currentIndex, setCurrentIndex] = useState(0);
+ // const { data, error, isLoading } = useSearchRecipeQuery({
+ // keyword,
+ // number: RESULTS_PER_PAGE,
+ // offset: (currentIndex - 1) * RESULTS_PER_PAGE,
+ // });
+ // console.log(data);
+ return (
+
+
+ {data.map(({ id, title }) => (
+ - {title}
+ ))}
+
+
+ );
+};
+
+export async function getServerSideProps(context) {
+ const { keyword } = context.query;
+ const { results: data } = await fetch(`https://spoonacular-recipe-food-nutrition-v1.p.rapidapi.com//recipes/search`, {
+ headers: {
+ 'content-type': 'application/json',
+ 'x-rapidapi-host': process.env.NEXT_PUBLIC_RAPID_API_HOST,
+ 'x-rapidapi-key': process.env.NEXT_PUBLIC_RAPID_API_KEY,
+ },
+ params: {
+ query: keyword,
+ number: RESULTS_PER_PAGE,
+ offset: 0,
+ },
+ }).then((res) => res.json());
+ return {
+ props: { data },
+ };
+}
+export default Search;
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 42bc1cc..96ee6ae 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,2 +1,3 @@
export * from './style';
export * from './dom';
+export * from './tabbable';
diff --git a/src/utils/tabbable.ts b/src/utils/tabbable.ts
new file mode 100644
index 0000000..f5e4c5d
--- /dev/null
+++ b/src/utils/tabbable.ts
@@ -0,0 +1,57 @@
+const focusableSelector = `
+ a[href],
+ area[href],
+ button,
+ input,
+ select,
+ textarea,
+ iframe,
+ summary,
+ details,
+ video[controls],
+ audio[controls],
+ [contenteditable=""],
+ [contenteditable="true"],
+ [tabindex]
+`
+ .replace(/\n\s+/g, '')
+ .trim();
+
+export const isFocusable = (elementNode: HTMLElement): boolean => {
+ const current = document.activeElement;
+ if (current === elementNode) return true;
+
+ const protectEvent = (e) => e.stopImmediatePropagation();
+ elementNode.addEventListener('focus', protectEvent, true);
+ elementNode.addEventListener('blur', protectEvent, true);
+ elementNode.focus({ preventScroll: true });
+
+ const result = document.activeElement === elementNode;
+ elementNode.blur();
+
+ if (current) current.focus({ preventScroll: true });
+ elementNode.removeEventListener('focus', protectEvent, true);
+ elementNode.removeEventListener('blur', protectEvent, true);
+
+ return result;
+};
+
+export const getFocusableElements = (node: HTMLElement): HTMLElement[] => getElements(node, true);
+
+/* -------------------------------------------------------------------------- */
+/* TABBABLE (tabindex="-1" 제외) */
+/* -------------------------------------------------------------------------- */
+
+const tabbableSelector = focusableSelector.replace(/\[tabindex\]/, '[tabindex]:not([tabindex^="-"])');
+
+export const isTabbable = (elementNode: HTMLElement) =>
+ isFocusable(elementNode) && Number(elementNode.getAttribute('tabindex')) >= 0;
+
+export const getTabbableElements = (node: HTMLElement) => getElements(node);
+
+/* -------------------------------------------------------------------------- */
+/* GET ELEMENTS */
+/* -------------------------------------------------------------------------- */
+
+const getElements = (node: HTMLElement, isFocusable = false): HTMLElement[] =>
+ Array.from(node.querySelectorAll(isFocusable ? focusableSelector : tabbableSelector));