-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
363 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
API_KEY = #뉴욕타임즈 API KEY | ||
REACT_APP_API_KEY = #뉴욕타임즈 API KEY |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,27 @@ | ||
import React from 'react'; | ||
import styled from 'styled-components/macro'; | ||
import { Tabs } from 'antd'; | ||
import SearchPage from './pages/SearchPage'; | ||
|
||
const { TabPane } = Tabs; | ||
|
||
const App = function (): JSX.Element { | ||
return ( | ||
<StyledWrapper> | ||
<h1>Hello World</h1> | ||
<Tabs defaultActiveKey="1"> | ||
<TabPane tab="SEARCH" key="1"> | ||
<SearchPage /> | ||
</TabPane> | ||
<TabPane tab="FAVORITE" key="2"> | ||
Content of Tab Pane 2 | ||
</TabPane> | ||
</Tabs> | ||
</StyledWrapper> | ||
); | ||
}; | ||
|
||
const StyledWrapper = styled.section``; | ||
const StyledWrapper = styled.section` | ||
padding: 1rem 1.5rem; | ||
`; | ||
|
||
export default App; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import axios, { AxiosPromise } from 'axios'; | ||
|
||
axios.defaults.baseURL = 'https://api.nytimes.com/svc/search/v2'; | ||
axios.defaults.params = { | ||
'api-key': process.env.REACT_APP_API_KEY, | ||
}; | ||
|
||
export default { | ||
searchArticles: (query: string): AxiosPromise => { | ||
return axios.get(`/articlesearch.json?q=${query}`); | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import React from 'react'; | ||
import styled from 'styled-components/macro'; | ||
import useImage from '../hooks/useImage'; | ||
|
||
import { sliceCharactersUntilNum } from '../utils'; | ||
|
||
interface ArticleListProps { | ||
article: Article; | ||
imageUrl: string; | ||
} | ||
|
||
const Article = function ({ | ||
article, | ||
imageUrl, | ||
}: ArticleListProps): JSX.Element { | ||
const Image = useImage(imageUrl); | ||
|
||
return ( | ||
<ArticleContainer> | ||
<TextWrapper> | ||
<h2>{article.headline.main}</h2> | ||
<p> | ||
{sliceCharactersUntilNum(article.lead_paragraph, 30) + ' '} | ||
<a href={article.web_url} target="_blank" rel="noreferrer"> | ||
...more | ||
</a> | ||
</p> | ||
</TextWrapper> | ||
<ImageWrapper> | ||
<Image /> | ||
</ImageWrapper> | ||
</ArticleContainer> | ||
); | ||
}; | ||
|
||
const ArticleContainer = styled.section` | ||
width: 100%; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
border-bottom: 1px solid lightgray; | ||
padding: 1rem 0rem; | ||
gap: 2rem; | ||
`; | ||
|
||
const TextWrapper = styled.section` | ||
width: 80%; | ||
overflow: hidden; | ||
h2 { | ||
margin: 0; | ||
padding: 0; | ||
font-size: 1.2rem; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
white-space: nowrap; | ||
} | ||
`; | ||
|
||
const ImageWrapper = styled.section` | ||
min-width: 100px; | ||
width: 20%; | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
`; | ||
|
||
export default Article; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React from 'react'; | ||
import styled from 'styled-components/macro'; | ||
import Article from './Article'; | ||
|
||
interface ArticleListProps { | ||
articles: Article[]; | ||
} | ||
|
||
const ArticleList = function ({ articles }: ArticleListProps): JSX.Element { | ||
return ( | ||
<ArticleListContainer> | ||
{articles.map(article => { | ||
const domain = 'https://nytimes.com/'; | ||
const hasImage = article.multimedia[0] !== undefined; | ||
const imageUrl = hasImage | ||
? `${domain}${article.multimedia[0]?.url}` | ||
: 'https://i.ibb.co/0yYnWSn/default-fallback-image.png'; | ||
return ( | ||
<Article key={article._id} article={article} imageUrl={imageUrl} /> | ||
); | ||
})} | ||
</ArticleListContainer> | ||
); | ||
}; | ||
|
||
const ArticleListContainer = styled.section` | ||
width: 85vw; | ||
max-width: 38rem; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: center; | ||
`; | ||
|
||
export default ArticleList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import React, { useState, useEffect } from 'react'; | ||
import styled from 'styled-components/macro'; | ||
|
||
interface SearchBarProps { | ||
onSearchSubmit: (term: string) => Promise<void>; | ||
clearResults: () => void; | ||
} | ||
|
||
const SearchBar = function ({ | ||
onSearchSubmit, | ||
clearResults, | ||
}: SearchBarProps): JSX.Element { | ||
const [term, setTerm] = useState(''); | ||
const [debouncedTerm, setDebouncedTerm] = useState(term); | ||
|
||
useEffect(() => { | ||
const timer = setTimeout(() => setTerm(debouncedTerm), 1000); | ||
return () => clearTimeout(timer); | ||
}, [debouncedTerm]); | ||
|
||
useEffect(() => { | ||
if (term !== '') { | ||
onSearchSubmit(term); | ||
} else { | ||
clearResults(); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [term]); | ||
|
||
return ( | ||
<Input | ||
onChange={({ target }) => setDebouncedTerm(target.value)} | ||
type="text" | ||
value={debouncedTerm} | ||
maxLength={100} | ||
/> | ||
); | ||
}; | ||
|
||
const Input = styled.input` | ||
width: 85vw; | ||
height: 20vh; | ||
max-height: 3.5rem; | ||
max-width: 38rem; | ||
padding: 1rem; | ||
`; | ||
|
||
export default SearchBar; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import React, { useState } from 'react'; | ||
import { Spin } from 'antd'; | ||
import styled from 'styled-components/macro'; | ||
|
||
type SpinSize = 'default' | 'small' | 'large'; | ||
|
||
const useImage = function ( | ||
src: string, | ||
spinSize = 'default', | ||
): () => JSX.Element { | ||
const [isLoading, setIsLoading] = useState(true); | ||
const Image = function () { | ||
return ( | ||
<> | ||
{isLoading && <Spin size={spinSize as SpinSize} />} | ||
<StyledImage | ||
isLoading={isLoading} | ||
src={src} | ||
alt="article_image" | ||
onLoad={() => setIsLoading(false)} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
return Image; | ||
}; | ||
|
||
const StyledImage = styled.img<{ isLoading: boolean }>` | ||
max-width: 100%; | ||
object-fit: cover; | ||
display: ${({ isLoading }) => isLoading && 'none'}; | ||
`; | ||
|
||
export default useImage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import React, { useState } from 'react'; | ||
import styled from 'styled-components/macro'; | ||
import ArticleList from '../components/ArticleList'; | ||
import SearchBar from '../components/SearchBar'; | ||
import API from '../api'; | ||
|
||
const SearchPage = function (): JSX.Element { | ||
const [articles, setArticles] = useState<Article[]>([]); | ||
const [noResults, setNoResults] = useState(false); | ||
|
||
const onSearchSubmit = async (term: string) => { | ||
const { data } = await API.searchArticles(term.toLowerCase()); | ||
setNoResults(data.response.docs.length === 0); | ||
setArticles(data.response.docs); | ||
}; | ||
|
||
const clearResults = () => setArticles([]); | ||
|
||
return ( | ||
<Container> | ||
<h1>SEARCH NY-TIMES</h1> | ||
<section> | ||
<SearchBar | ||
onSearchSubmit={onSearchSubmit} | ||
clearResults={clearResults} | ||
/> | ||
</section> | ||
<section> | ||
{noResults ? ( | ||
<h3 className="no-results">No results found.</h3> | ||
) : ( | ||
<ArticleList articles={articles} /> | ||
)} | ||
</section> | ||
</Container> | ||
); | ||
}; | ||
|
||
const Container = styled.section` | ||
position: relative; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: center; | ||
gap: 1rem; | ||
`; | ||
|
||
export default SearchPage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
type HeadlineKey = | ||
| 'main' | ||
| 'kicker' | ||
| 'content_kicker' | ||
| 'print_headline' | ||
| 'name' | ||
| 'seo' | ||
| 'sub'; | ||
|
||
type PersonKey = | ||
| 'firstname' | ||
| 'middlename' | ||
| 'lastname' | ||
| 'qualifier' | ||
| 'title' | ||
| 'role' | ||
| 'organization' | ||
| 'rank'; | ||
|
||
type MetaKey = 'hits' | 'offset' | 'time'; | ||
|
||
interface Legacy { | ||
xlarge: string; | ||
xlargewidth: integer; | ||
xlargeheight: integer; | ||
} | ||
|
||
interface Multimedia { | ||
rank: integer; | ||
subtype: string; | ||
caption: string; | ||
credit: string; | ||
type: string; | ||
url: string; | ||
height: integer; | ||
width: integer; | ||
legacy: Legacy; | ||
crop_name: string; | ||
} | ||
|
||
interface Keywords { | ||
name: string; | ||
value: string; | ||
rank: integer; | ||
major: string; | ||
} | ||
|
||
interface Byline { | ||
original: string; | ||
person: { [key in PersonKey]: string | integer }[]; | ||
organization: string; | ||
} | ||
|
||
interface Article { | ||
abstract: string; | ||
byline: Byline; | ||
document_type: string; | ||
headline: Record<HeadlineKey, string>; | ||
keywords: Keywords[]; | ||
lead_paragraph: string; | ||
multimedia: Multimedia[]; | ||
news_desk: string; | ||
pub_date: string; | ||
section_name: string; | ||
snippet: string; | ||
source: string; | ||
subsection_name: string; | ||
type_of_material: string; | ||
uri: string; | ||
web_url: string; | ||
word_count: integer; | ||
_id: string; | ||
} | ||
|
||
interface Response { | ||
docs: Article[]; | ||
meta: Record<MetaKey, integer>; | ||
} | ||
|
||
interface ArticleSearch { | ||
status: string; | ||
copyright: string; | ||
response: Response; | ||
} |
Empty file.
Oops, something went wrong.