diff --git a/.DS_Store b/.DS_Store index 5008ddf..63f1315 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..391bfe6 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,26 @@ +version: 2.1 + +orbs: + node: circleci/node@5.0.0 + +jobs: + build-test: + executor: + name: node/default + tag: 14.15.1 + steps: + - checkout + - run: node --version + - node/install-packages: + app-dir: ~/project + override-ci-command: npm install + - run: sudo npm install -g npm@latest + - run: + name: "Run tests" + command: npm test + +workflows: + version: 2.1 + test_and_release: + jobs: + - build-test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/package.json b/package.json new file mode 100644 index 0000000..d942982 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "frontend-coding-challenge", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.16.1", + "@testing-library/react": "^12.1.2", + "@testing-library/user-event": "^13.5.0", + "axios": "^0.24.0", + "mobx": "^6.3.9", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-scripts": "5.0.0", + "web-vitals": "^2.1.2" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..a11777c Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..aa069f2 --- /dev/null +++ b/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/public/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..080d6c7 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..3784575 --- /dev/null +++ b/src/App.js @@ -0,0 +1,25 @@ +import logo from './logo.svg'; +import './App.css'; + +function App() { + return ( +
+
+ logo +

+ Edit src/App.js and save to reload. +

+ + Learn React + +
+
+ ); +} + +export default App; diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..ec2585e --- /dev/null +++ b/src/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..ef2edf8 --- /dev/null +++ b/src/index.js @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import './index.css'; +import App from './App'; +import reportWebVitals from './reportWebVitals'; + +ReactDOM.render( + + + , + document.getElementById('root') +); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/src/store/githubRepo/index.ts b/src/store/githubRepo/index.ts new file mode 100644 index 0000000..4f44c19 --- /dev/null +++ b/src/store/githubRepo/index.ts @@ -0,0 +1,4 @@ +export * from "./types"; +export * from "./repository"; +export * from "./store"; + diff --git a/src/store/githubRepo/repository.ts b/src/store/githubRepo/repository.ts new file mode 100644 index 0000000..a07b920 --- /dev/null +++ b/src/store/githubRepo/repository.ts @@ -0,0 +1,16 @@ +import axios from "axios"; +import { GithubRepoInputOptions, GithubRepoType } from "./types"; + +export interface IGithubRepoRepository { + load(options: GithubRepoInputOptions): Promise; + } + const BaseUrl = "https://api.github.com/search/repositories"; + + export function GithubRepoRepository(): IGithubRepoRepository { + return { + load: async (options) => { + const url = `${BaseUrl}?q=created:>${options.date}&sort=${options.sort}&order=${options.order}&page=${options.page}` + const result = await axios({url , method: 'GET'}) + return result.data; + } + }} diff --git a/src/store/githubRepo/store.ts b/src/store/githubRepo/store.ts new file mode 100644 index 0000000..997e439 --- /dev/null +++ b/src/store/githubRepo/store.ts @@ -0,0 +1,61 @@ +import { action, observable, flow } from "mobx"; +import { CancellablePromise } from "mobx/lib/api/flow"; +import { IRootStore } from "../root/store"; +import { GithubRepoRepository } from "./repository"; + +import { GithubRepoInputOptions, GithubRepoType } from "./types"; + +export interface IGithubRepoStore { + repos: GithubRepoType[] | null; + load: (options: GithubRepoInputOptions) => void; + loading: boolean; + error: Error | null; + clearError: () => void; + clear: () => void; +} + +export function GithubRepoStore(rootStore: IRootStore) { + const _repository = GithubRepoRepository(); + let _currentLoad: CancellablePromise | null = null; + const _cancelLoad = () => { + if (_currentLoad !== null) { + _currentLoad.catch(() => null); + _currentLoad.cancel(); + _currentLoad = null; + } + } + + const _load = flow(async function*(options: GithubRepoInputOptions) { + store.loading = true; + store.repos = null; + try { + const repos = yield _repository.load(options); + store.repos = repos; + } catch (error) { + store.error = error; + } + store.loading = false; + }); + + const store: IGithubRepoStore = observable({ + repos: null, + load: action((options) => { + _cancelLoad(); + _currentLoad = _load(options); + }), + loading: false, + error: null, + clearError: action(() => { + store.error = null; + }), + clear: action(() => { + _cancelLoad(); + if (store.loading) { + store.loading = false; + } + }), + + }); + + return store; +} \ No newline at end of file diff --git a/src/store/githubRepo/types.ts b/src/store/githubRepo/types.ts new file mode 100644 index 0000000..fec2789 --- /dev/null +++ b/src/store/githubRepo/types.ts @@ -0,0 +1,18 @@ + +export interface GithubRepoType { + owner:string; + name:string; + description:string; + has_issues:boolean; + open_issues_count:string; + stargazers_count:string; + created_at:string; +} + +export interface GithubRepoInputOptions { + date:string; + sort: string; + order: string; + page: number|1; + + } \ No newline at end of file diff --git a/src/store/root/store.ts b/src/store/root/store.ts new file mode 100644 index 0000000..d5fc767 --- /dev/null +++ b/src/store/root/store.ts @@ -0,0 +1,30 @@ +import React from "react"; +import { GithubRepoStore, IGithubRepoStore } from "../githubRepo/store"; + +export interface IRootStore { + githubRepoStore: IGithubRepoStore; + // other stores should come here +} + +export function RootStore() { + const lazyStore = (factory: (root: IRootStore) => S) => { + let _store: S | null = null; + return () => { + if (_store === null) { + _store = factory(store); + } + return _store; + }; + }; + + const _githubRepoStore = lazyStore(GithubRepoStore); + + const store: IRootStore = { + get githubRepoStore() { + return _githubRepoStore(); + } + }; + return store; + } + + export const RootStoreContext = React.createContext(RootStore()); \ No newline at end of file diff --git a/src/store/utils/dateUtils.ts b/src/store/utils/dateUtils.ts new file mode 100644 index 0000000..3c941a1 --- /dev/null +++ b/src/store/utils/dateUtils.ts @@ -0,0 +1,16 @@ + +export function getPreviousDateFromDays(days:number) { + let today = new Date() + var priorDate = new Date().setDate(today.getDate() - days) + let d = new Date(priorDate) + + let month = addPadToDate(d.getMonth() + 1), + day = addPadToDate(d.getDate()), + year = `${d.getFullYear()}`; + + return [year, month, day].join('-'); +} + +function addPadToDate(date:number) { + return date < 10 ? '0' + date : '' + date; + } \ No newline at end of file