Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .storybook/preview-body.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
<div id="loading-start" aria-live="assertive"></div>
<div id="loading-end" aria-live="assertive"></div>
<div id="dialog"></div>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions src/components/Auth/Auth.styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
53 changes: 29 additions & 24 deletions src/components/Auth/Auth.tsx
Original file line number Diff line number Diff line change
@@ -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<FormValues>): JSX.Element => {
const { currentForm, values, errors, dirty, touched, isValid, handleChange, handleBlur, handleSubmit } = props;
return (
<form onSubmit={handleSubmit}>
{FIELDS[currentForm].map((field): JSX.Element => (
<>
<label className="a11yHidden" htmlFor={field}>
{field.toUpperCase()}
</label>
<StyledInput
id={field}
$warning={touched[field] && errors[field]}
name={field}
placeholder={PLACEHOLDER[field]}
type={TYPE[field]}
onChange={handleChange}
onBlur={handleBlur}
value={values[field] || ''}
/>
<StyledFieldError>{touched[field] && errors[field]}</StyledFieldError>
</>
))}
<StyledAuthContainer>
<StyledForm onSubmit={handleSubmit}>
{FIELDS[currentForm].map(
(field): JSX.Element => (
<>
<label className="a11yHidden" htmlFor={field}>
{field.toUpperCase()}
</label>
<StyledInput
id={field}
$warning={touched[field] && errors[field]}
name={field}
placeholder={PLACEHOLDER[field]}
type={TYPE[field]}
onChange={handleChange}
onBlur={handleBlur}
value={values[field] || ''}
/>
<StyledFieldError>{touched[field] && errors[field]}</StyledFieldError>
</>
),
)}

<button type="submit" disabled={!dirty || !isValid}>
{HEADING[currentForm]}
</button>
</form>
<Button variant="filled" round={true} color="primaryGreen" type="submit" disabled={!dirty || !isValid}>
{HEADING[currentForm]}
</Button>
</StyledForm>
</StyledAuthContainer>
);
};

Expand Down
4 changes: 4 additions & 0 deletions src/components/Button/Button.styled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const StyledButton = styled.button<StyledButtonProps>`
border: none;
background: none;
border-radius: ${({ round }) => round && '30px'};
&:disabled {
background-color: ${({ theme }) => theme.color.gray300};
cursor: not-allowed;
}
`;

export const StyledOutlineButton = styled(StyledButton)`
Expand Down
1 change: 1 addition & 0 deletions src/components/Button/Button.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface StyledButtonProps {
className?: string;
onClick?: () => void;
title?: string;
disabled?: boolean;
}

export interface ButtonProps extends StyledButtonProps {
Expand Down
21 changes: 21 additions & 0 deletions src/components/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -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: <h2>hahaha</h2>,
nodeId: 'dialog',
label: 'test',
},
} as ComponentMeta<typeof Dialog>;

const Template: ComponentStory<typeof Dialog> = (args) => <Dialog {...args} />;

export const DefaultDialog = Template.bind({});

export const TestDialog = Template.bind({});

TestDialog.args = { ...DefaultDialog.args, children: <h2>TESTTEST</h2> };
46 changes: 46 additions & 0 deletions src/components/Dialog/Dialog.styled.tsx
Original file line number Diff line number Diff line change
@@ -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);
`;
79 changes: 79 additions & 0 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -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(
<>
<StyledDialogContainer
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-hidden="false"
aria-label={`${label} Dialog`}
{...restProps}
>
<StyledDialogContent>{children}</StyledDialogContent>
<StyledCloseButton
ariaLabel={`Close ${label} dialog.`}
iconType="close"
type="button"
variant="transparent"
color="white"
size="large"
onClick={onClose}
/>
</StyledDialogContainer>
<StyledDim role="presentation" onClick={onClose} />
</>,
document.getElementById(nodeId),
);
}
9 changes: 9 additions & 0 deletions src/components/Dialog/Dialog.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

export interface DialogProps {
onClose: () => void;
children: React.ReactNode;
nodeId?: string;
label: string;
[restProps: string]: any;
}
17 changes: 17 additions & 0 deletions src/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Html, Head, Main, NextScript } from 'next/document';
import { useContext } from 'react';

export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<div id="loading-start" aria-live="assertive"></div>
<div id="loading-end" aria-live="assertive"></div>
<div id="dialog"></div>
<NextScript />
</body>
</Html>
);
}
47 changes: 47 additions & 0 deletions src/pages/search/[keyword].tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<ul>
{data.map(({ id, title }) => (
<li key={id}>{title}</li>
))}
</ul>
</div>
);
};

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;
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './style';
export * from './dom';
export * from './tabbable';
Loading