├── .gitignore ├── public ├── _redirects ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── space_kitty_pattern.png ├── manifest.json └── index.html ├── src ├── assets │ ├── cat_logo.png │ ├── cat_logo@2x.png │ ├── space_cat_logo.png │ ├── space_cat_logo@2x.png │ └── space_kitty_pattern.svg ├── components │ ├── index.js │ ├── content-section.js │ ├── __tests__ │ │ ├── query-result.js │ │ ├── track-detail.js │ │ ├── module-detail.js │ │ └── modules-navigation.js │ ├── md-content.js │ ├── footer.js │ ├── query-result.js │ ├── layout.js │ ├── module-detail.js │ ├── header.js │ ├── modules-navigation.js │ └── track-detail.js ├── pages │ ├── index.js │ ├── tracks.js │ ├── track.js │ ├── module.js │ └── __tests__ │ │ └── tracks.js ├── index.js ├── utils │ ├── helpers.js │ ├── useWindowDimensions.js │ └── test-utils.js ├── containers │ ├── __tests__ │ │ └── track-card.js │ └── track-card.js └── styles.js ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .env -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NansD/odyssey-lift-off-part5-client/main/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NansD/odyssey-lift-off-part5-client/main/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NansD/odyssey-lift-off-part5-client/main/public/logo512.png -------------------------------------------------------------------------------- /src/assets/cat_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NansD/odyssey-lift-off-part5-client/main/src/assets/cat_logo.png -------------------------------------------------------------------------------- /src/assets/cat_logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NansD/odyssey-lift-off-part5-client/main/src/assets/cat_logo@2x.png -------------------------------------------------------------------------------- /public/space_kitty_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NansD/odyssey-lift-off-part5-client/main/public/space_kitty_pattern.png -------------------------------------------------------------------------------- /src/assets/space_cat_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NansD/odyssey-lift-off-part5-client/main/src/assets/space_cat_logo.png -------------------------------------------------------------------------------- /src/assets/space_cat_logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NansD/odyssey-lift-off-part5-client/main/src/assets/space_cat_logo@2x.png -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Footer } from './footer'; 2 | export { default as Header } from './header'; 3 | export { default as Layout } from './layout'; 4 | export { default as ModuleDetail } from './module-detail'; 5 | export { default as QueryResult } from './query-result'; 6 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Router } from '@reach/router'; 3 | /** importing our pages */ 4 | import Tracks from './tracks'; 5 | import Track from './track'; 6 | import Module from './module'; 7 | 8 | export default function Pages() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import GlobalStyles from './styles'; 4 | import Pages from './pages'; 5 | import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client'; 6 | 7 | const client = new ApolloClient({ 8 | uri: 'http://localhost:4000', 9 | cache: new InMemoryCache(), 10 | }); 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ); 19 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Format seconds to human readable text in a compact form: 3 | * s, m or H:m (not m:s or H:m:s) 4 | */ 5 | export const humanReadableTimeFromSeconds = (seconds) => { 6 | if (seconds < 60) { 7 | return `${seconds}s`; 8 | } 9 | const totalMinutes = Math.floor(seconds / 60); 10 | let hours = Math.floor(totalMinutes / 60) || 0; 11 | const minutestoDisplay = totalMinutes % 60; 12 | let timeStr = ``; 13 | if (hours > 0) { 14 | timeStr += `${hours}h `; 15 | } 16 | timeStr += `${minutestoDisplay}m`; 17 | 18 | return timeStr; 19 | }; 20 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/content-section.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { widths, colors } from '../styles'; 4 | 5 | /** 6 | * Content Section component renders content (mainly text/mdown based) 7 | * for track and module details 8 | */ 9 | const ContentSection = ({ children }) => { 10 | return {children}; 11 | }; 12 | 13 | export default ContentSection; 14 | 15 | /** ContentSection styled component */ 16 | const ContentDiv = styled.div({ 17 | marginTop: 10, 18 | display: 'flex', 19 | flexDirection: 'column', 20 | maxWidth: widths.textPageWidth, 21 | width: '100%', 22 | alignSelf: 'center', 23 | backgroundColor: colors.background, 24 | }); 25 | -------------------------------------------------------------------------------- /src/utils/useWindowDimensions.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | function getWindowDimensions() { 4 | const { innerWidth: width, innerHeight: height } = window; 5 | return { 6 | width, 7 | height, 8 | }; 9 | } 10 | 11 | export default function useWindowDimensions() { 12 | const [windowDimensions, setWindowDimensions] = useState( 13 | getWindowDimensions() 14 | ); 15 | 16 | useEffect(() => { 17 | function handleResize() { 18 | setWindowDimensions(getWindowDimensions()); 19 | } 20 | 21 | window.addEventListener('resize', handleResize); 22 | return () => window.removeEventListener('resize', handleResize); 23 | }, []); 24 | 25 | return windowDimensions; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/test-utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/extend-expect'; 4 | import { MockedProvider } from '@apollo/client/testing'; 5 | 6 | const renderApollo = ( 7 | node, 8 | { mocks, addTypename, defaultOptions, cache, resolvers, ...options } 9 | ) => { 10 | return render( 11 | 19 | {node} 20 | , 21 | options 22 | ); 23 | }; 24 | 25 | export * from '@testing-library/react'; 26 | export { renderApollo }; 27 | -------------------------------------------------------------------------------- /src/components/__tests__/query-result.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '../../utils/test-utils'; 3 | import QueryResult from '../query-result'; 4 | 5 | describe('Query Result', () => { 6 | // automatically unmount and cleanup DOM after the test is finished. 7 | afterEach(cleanup); 8 | 9 | it('renders loading state', async () => { 10 | const { getByTestId } = render(); 11 | getByTestId(/spinner/i); 12 | }); 13 | 14 | it('renders No Data message', async () => { 15 | // passing no error and no data 16 | const { getByText } = render(); 17 | getByText(/nothing to show/i); 18 | }); 19 | 20 | it('renders Error', async () => { 21 | const { getByText } = render(); 22 | getByText(/you lose/i); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/md-content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { colors } from '../styles'; 4 | import ReactMarkdown from 'react-markdown'; 5 | 6 | /** 7 | * Markdown component is a simple style wrapper for markdown content used across our app 8 | */ 9 | const MarkDown = ({ content }) => { 10 | return ; 11 | }; 12 | 13 | export default MarkDown; 14 | 15 | /** Markdown styled components */ 16 | const StyledMarkdown = styled(ReactMarkdown)({ 17 | color: colors.grey.darker, 18 | 19 | h1: { 20 | fontSize: '1.7em', 21 | }, 22 | h2: { 23 | fontSize: '1.4em', 24 | }, 25 | h3: { 26 | fontSize: '1.2em', 27 | }, 28 | a: { 29 | color: colors.pink.base, 30 | }, 31 | pre: { 32 | padding: 20, 33 | borderRadius: 4, 34 | border: `solid 1px ${colors.silver.dark}`, 35 | backgroundColor: colors.silver.base, 36 | code: { 37 | fontSize: '0.9em', 38 | }, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { colors, ApolloIcon } from '../styles'; 4 | 5 | /** 6 | * Footer is useless component to make our app look a little closer to a real website! 7 | */ 8 | const Footer = ({ children }) => { 9 | return ( 10 | 11 | 2021 ©{' '} 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Footer; 20 | 21 | /** Footer styled components */ 22 | const FooterContainer = styled.div({ 23 | display: 'flex', 24 | flexDirection: 'row', 25 | justifyContent: 'center', 26 | alignItems: 'center', 27 | color: colors.pink.base, 28 | marginTop: 30, 29 | height: 200, 30 | padding: 20, 31 | backgroundColor: 'white', 32 | borderTop: `solid 1px ${colors.pink.light}`, 33 | }); 34 | 35 | const LogoContainer = styled.div({ 36 | height: 40, 37 | marginLeft: 5, 38 | svg: { 39 | height: 40, 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /src/components/query-result.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from '@emotion/styled'; 3 | import { LoadingSpinner } from '@apollo/space-kit/Loaders/LoadingSpinner'; 4 | 5 | /** 6 | * Query Results conditionally renders Apollo useQuery hooks states: 7 | * loading, error or its children when data is ready 8 | */ 9 | const QueryResult = ({ loading, error, data, children }) => { 10 | if (error) { 11 | return

ERROR: {error.message}

; 12 | } 13 | if (loading) { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | if (!data) { 21 | return

Nothing to show...

; 22 | } 23 | if (data) { 24 | return children; 25 | } 26 | }; 27 | 28 | export default QueryResult; 29 | 30 | /** Query Result styled components */ 31 | const SpinnerContainer = styled.div({ 32 | display: 'flex', 33 | justifyContent: 'center', 34 | alignItems: 'center', 35 | width: '100%', 36 | height: '100vh', 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Header, Footer } from '../components'; 3 | import styled from '@emotion/styled'; 4 | import { widths, unit } from '../styles'; 5 | 6 | /** 7 | * Layout renders the full page content: 8 | * with header, Page container and footer 9 | */ 10 | const Layout = ({ fullWidth, children, grid }) => { 11 | return ( 12 | <> 13 |
14 | 15 | {children} 16 | 17 |