├── .gitignore ├── .static ├── README.md ├── app.json ├── package-lock.json ├── package.json ├── public ├── index.html └── manifest.json └── src ├── App.js ├── App.test.js ├── back.jpg ├── back1.svg ├── bookdiary ├── 1080p Card.png ├── Landing Page.png ├── iPhone 6-7-8 – 1.png └── iPhone 6-7-8 – 2.png ├── components ├── AuthorCard.js ├── Card.js ├── Cards.js ├── CategoryCard.js ├── Icon.js ├── Input.js ├── LandingPage.js ├── LandingPageContent.js ├── Menu.js ├── MenuLikes.js ├── SearchList.js └── SinglePageCard.js ├── config.js ├── css └── index.css ├── index.js ├── serviceWorker.js ├── store ├── actions │ └── BooksAction.js └── reducers │ └── rootReducer.js └── strip.png /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /.static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-book-app/2f415225e775fb722b69251da350ee5701c3c9fc/.static -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Book App 2 | ![React Book App](https://cosmic-s3.imgix.net/237c3030-e788-11e8-a0f7-f393606b1cf1-react-book-app-4.png?w=1200) 3 | # [Demo](https://cosmicjs.com/apps/react-book-app) 4 | Uses: 5 | - React for UI 6 | - Redux for state mangaement 7 | - [Cosmic JS](https://cosmicjs.com) for content management 8 | - HTML 9 | - CSS 10 | - AdobeXD 11 | - (RWD - mobile, tablet, desktop) / css-grid, media 12 | 13 | 14 | ## Screenshots 15 | ![React Book App](https://cosmic-s3.imgix.net/e28ef120-e787-11e8-a8c8-ad6b576b471b-react-book-app-1.png?w=800) 16 | ![React Book App](https://cosmic-s3.imgix.net/1ad40070-e788-11e8-a0f7-f393606b1cf1-react-book-app-2.png?w=800) 17 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dokku": { 4 | "predeploy": "npm run build" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-book-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.6.0", 7 | "react-dom": "^16.6.0", 8 | "react-redux": "^5.1.0", 9 | "react-router-dom": "^4.3.1", 10 | "react-scripts": "2.1.1", 11 | "redux": "^4.0.1", 12 | "redux-thunk": "^2.3.0", 13 | "sal.js": "^0.5.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 26 | React App 27 | 28 | 29 | 32 |
33 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /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 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {BrowserRouter, Route, Switch} from 'react-router-dom' 3 | import { connect } from 'react-redux'; 4 | import sal from 'sal.js'; 5 | 6 | import LandingPage from './components/LandingPage'; 7 | import { FetchApiBooks } from './store/actions/BooksAction'; 8 | import Icon from './components/Icon'; 9 | import SinglePageCard from './components/SinglePageCard'; 10 | import CategoryCard from './components/CategoryCard'; 11 | import AuthorCard from './components/AuthorCard'; 12 | 13 | class App extends Component { 14 | 15 | componentDidMount () { 16 | 17 | this.props.fetchBooks(); 18 | sal({ 19 | threshold: 1, 20 | once: false, 21 | }); 22 | 23 | } 24 | render() { 25 | return ( 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | ); 39 | } 40 | } 41 | 42 | const mapDispatchToProps = (dispatch) => { 43 | return{ 44 | fetchBooks: () => { 45 | dispatch(FetchApiBooks()); 46 | } 47 | } 48 | } 49 | export default connect(null,mapDispatchToProps)(App); 50 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/back.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-book-app/2f415225e775fb722b69251da350ee5701c3c9fc/src/back.jpg -------------------------------------------------------------------------------- /src/back1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/bookdiary/1080p Card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-book-app/2f415225e775fb722b69251da350ee5701c3c9fc/src/bookdiary/1080p Card.png -------------------------------------------------------------------------------- /src/bookdiary/Landing Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-book-app/2f415225e775fb722b69251da350ee5701c3c9fc/src/bookdiary/Landing Page.png -------------------------------------------------------------------------------- /src/bookdiary/iPhone 6-7-8 – 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-book-app/2f415225e775fb722b69251da350ee5701c3c9fc/src/bookdiary/iPhone 6-7-8 – 1.png -------------------------------------------------------------------------------- /src/bookdiary/iPhone 6-7-8 – 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-book-app/2f415225e775fb722b69251da350ee5701c3c9fc/src/bookdiary/iPhone 6-7-8 – 2.png -------------------------------------------------------------------------------- /src/components/AuthorCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux'; 3 | import Cards from './Cards'; 4 | import LandingPageContent from './LandingPageContent'; 5 | function AuthorCard(props) { 6 | 7 | return ( 8 |
9 | {props.author && } 10 | {props.booksList ? : } 11 |
12 | ) 13 | } 14 | const mapStateToProps = (state, ownProps) => { 15 | const author = ownProps.match.params.author_slug; 16 | console.log('author', state); 17 | return{ 18 | booksList:state.books.filter(cat => { 19 | return cat.metadata.author.slug === author 20 | }), 21 | author: state.author.find(item => {return(item.slug === author)}) 22 | } 23 | } 24 | 25 | export default connect(mapStateToProps)(AuthorCard); 26 | -------------------------------------------------------------------------------- /src/components/Card.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-book-app/2f415225e775fb722b69251da350ee5701c3c9fc/src/components/Card.js -------------------------------------------------------------------------------- /src/components/Cards.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Link } from 'react-router-dom' 4 | 5 | const Cards = (props) => { 6 | 7 | const Save = (item) => { 8 | props.hitLike(item); 9 | } 10 | 11 | return ( 12 | 13 |
14 | 15 | { 16 | props.booksList.map((item, index) => { 17 | return( 18 |
22 |
23 | 24 |
25 |
26 |
    27 |
  • Save(index, item.slug)} > 28 | 29 |
  • 30 |
  • 31 |
  • 32 |
33 |
34 |
35 |
36 | 37 |

{item.title}

38 | 39 |
40 |
Author: {item.metadata.author.title}
41 |
Genres: {item.metadata.category.title}
42 |
43 | ) 44 | }) 45 | } 46 |
47 | ) 48 | } 49 | const mapStateToProps= (state) => { 50 | return{ 51 | like: state.likes 52 | 53 | } 54 | } 55 | const mapDispatchToProps = (dispatch) => { 56 | return{ 57 | hitLike: (item) => { 58 | dispatch({type:'HANDLE_LIKE', like: item }) 59 | }, 60 | hitAdd: (item) => { 61 | dispatch({type:'HANDLE_ADD', add: item }) 62 | } 63 | } 64 | } 65 | export default connect(mapStateToProps, mapDispatchToProps)(Cards); 66 | -------------------------------------------------------------------------------- /src/components/CategoryCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import Cards from './Cards'; 4 | import LandingPageContent from './LandingPageContent'; 5 | 6 | function CategoryCard(props) { 7 | 8 | return ( 9 |
10 | {props.category && } 11 | {props.booksList ? : } 12 |
13 | ) 14 | } 15 | const mapStateToProps = (state, ownProps) => { 16 | const category = ownProps.match.params.category_slug; 17 | return{ 18 | booksList:state.books.filter(cat => { 19 | // console.log("from map", cat.metadata.category.slug); 20 | return cat.metadata.category.slug === category 21 | }), 22 | category: state.menu.find(item => (item.slug === category)) 23 | } 24 | } 25 | export default connect(mapStateToProps)(CategoryCard); 26 | -------------------------------------------------------------------------------- /src/components/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Menu from './Menu'; 3 | import MenuLikes from './MenuLikes'; 4 | import Input from './Input'; 5 | import { Link } from 'react-router-dom'; 6 | import SearchList from './SearchList'; 7 | 8 | export default function Icon() { 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Input.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | class Input extends Component { 4 | render() { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | } 12 | const mapStateToProps = (state) => { 13 | return{ 14 | input: state.input 15 | } 16 | } 17 | const mapDispatchToProps = (dispatch) => { 18 | return{ 19 | handleInput: (e) => { 20 | dispatch({type:'INPUT_CHANGE', input:e.target.value}) 21 | } 22 | } 23 | } 24 | export default connect(mapStateToProps, mapDispatchToProps)(Input); 25 | -------------------------------------------------------------------------------- /src/components/LandingPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Cards from './Cards'; 3 | import { Link } from 'react-router-dom'; 4 | import { connect } from 'react-redux'; 5 | 6 | 7 | const LandingPage = (props)=>{ 8 | return ( 9 |
10 |
11 | 12 |
16 |

Books Diary

17 |

" Gabriel.s small cottage was lit by a single candle that cast light on a collection of weapons along one wall and a few books on a bookshelf on another "

18 |
19 | 20 |
21 |

Favourite

22 | 23 |
24 | ) 25 | } 26 | const mapStateToProps = (state) => { 27 | return { 28 | booksList: state.books 29 | } 30 | } 31 | export default connect(mapStateToProps)(LandingPage) 32 | -------------------------------------------------------------------------------- /src/components/LandingPageContent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom'; 3 | 4 | export default function LandingPageContent(props) { 5 | return ( 6 |
7 | 8 | 9 |
10 |

{props.page}

11 | {props.body ? :

Loading...

} 12 |
13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux'; 3 | import { NavLink} from 'react-router-dom'; 4 | class Menu extends Component { 5 | 6 | componentDidMount () { 7 | 8 | } 9 | render() { 10 | const { menuCategory } = this.props 11 | console.log("From component", this.props.menuCategory); 12 | return ( 13 |
14 | 23 |
24 | ) 25 | } 26 | } 27 | 28 | const mapStateToProps = (state) => { 29 | 30 | return { 31 | menuCategory: state.menu 32 | } 33 | } 34 | export default connect(mapStateToProps)(Menu); 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/MenuLikes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function MenuLikes() { 4 | return ( 5 |
6 | 16 |
17 | ) 18 | } 19 | 20 | // import React, { Component } from 'react' 21 | 22 | // export default class MenuLikes extends Component { 23 | 24 | // render() { 25 | // console.log(this.props); 26 | // const Save = () => { 27 | // localStorage.setItem("Like", "hey" ); 28 | // } 29 | // return ( 30 | //
31 | // 36 | //
37 | // ) 38 | // } 39 | // } 40 | -------------------------------------------------------------------------------- /src/components/SearchList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Link } from 'react-router-dom'; 4 | function SearchList(props) { 5 | return ( 6 |
7 | { 8 | props.value && props.lists.map((item, index) => { 9 | return( 10 | 11 |
{item.title}
12 |
{item.metadata.author.title}
13 | 14 | ) 15 | }) 16 | } 17 |
18 | ) 19 | } 20 | const mapStateToProps = (state) => { 21 | return{ 22 | lists: state.search, 23 | value: state.value 24 | } 25 | } 26 | 27 | export default connect(mapStateToProps)(SearchList); 28 | -------------------------------------------------------------------------------- /src/components/SinglePageCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux'; 3 | 4 | import { Link } from 'react-router-dom'; 5 | 6 | function SinglePageCard(props) { 7 | 8 | return ( 9 | props.book ? 10 |
14 | 15 |
16 |
{} 17 |
18 |
19 |

{props.book.title}

20 | 21 |
22 | Go back 23 |
24 | 25 | 26 |
27 | { props.book.metadata.category.title} 28 |
29 | 30 |
31 |
: 32 | ) 33 | } 34 | const mapStateToProps = (state, ownProps) => { 35 | let slug = ownProps.match.params.item_slug; 36 | return{ 37 | book: state.books.find(item => item.slug === slug) 38 | } 39 | } 40 | export default connect(mapStateToProps)(SinglePageCard); -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | COSMIC_BUCKET: process.env.COSMIC_BUCKET || 'react-book-app', 3 | COSMIC_READ_KEY: process.env.COSMIC_READ_KEY || '' 4 | } -------------------------------------------------------------------------------- /src/css/index.css: -------------------------------------------------------------------------------- 1 | @import '/../node_modules/sal.js/dist/sal.css'; 2 | :root{ 3 | --clr1: #333533; 4 | --clr2: #242423; 5 | --clr3: #F5CB5C; 6 | --clr4: #E8EDDF; 7 | --clr5: #CFDBD5; 8 | } 9 | 10 | html{ 11 | margin: 0; 12 | padding: 0; 13 | } 14 | 15 | body { 16 | margin: 0; 17 | padding: 0; 18 | font-family: 'SourceSansPro', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 19 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 20 | sans-serif; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | background:var(--clr5); 24 | 25 | } 26 | .main-wrap h1 { 27 | color: rgb(48, 48, 48); 28 | font-family:'Playfair Display'; 29 | font-size: 5.5rem; 30 | font-weight: 900; 31 | } 32 | .main-wrap p { 33 | color: var(--clr1); 34 | font-weight: 400; 35 | font-size: 1.3rem; 36 | max-width: 80%; 37 | font-family: 'SourceSansPro', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 38 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue"; 39 | opacity:0.9; 40 | } 41 | 42 | 43 | a{ 44 | color:var(--clr2); 45 | text-decoration: none; 46 | } 47 | 48 | 49 | /* landing page */ 50 | h1,h2{ 51 | margin:0; 52 | padding:0; 53 | } 54 | 55 | 56 | .container { 57 | min-height: 100vh; 58 | width:100%; 59 | /* background:#F5CB5C; */ 60 | background: url('../back1.svg'); 61 | background-repeat: no-repeat; 62 | background-size: cover; 63 | display:flex; 64 | flex-direction: column; 65 | justify-content: center; 66 | align-items: center; 67 | 68 | 69 | } 70 | 71 | .main-slide{ 72 | 73 | text-align: center; 74 | padding: 20% 30px; 75 | width:80%; 76 | flex:4; 77 | display:flex; 78 | flex-direction: column; 79 | align-items: center; 80 | } 81 | .main-slide p{ 82 | text-align: center; 83 | } 84 | 85 | /* menu likes */ 86 | 87 | .like-menu{ 88 | 89 | background: var(--clr3); 90 | width:300px; 91 | margin-bottom: 5%; 92 | border-bottom-left-radius: 5px; 93 | border-bottom-right-radius: 5px; 94 | flex:1; 95 | } 96 | .like-menu ul{ 97 | list-style-type: none; 98 | display: flex; 99 | justify-content: center; 100 | align-items: center; 101 | 102 | } 103 | .like-menu li{ 104 | flex: 1; 105 | padding:0 20px; 106 | 107 | } 108 | .category-menu{ 109 | width:100%; 110 | flex:1; 111 | padding:5%; 112 | box-sizing: border-box; 113 | } 114 | .category-menu ul{ 115 | list-style-type: none; 116 | display: flex; 117 | justify-content: space-between; 118 | align-items: center; 119 | flex-wrap: wrap; 120 | text-align: center; 121 | padding: 0; 122 | } 123 | .category-menu li{ 124 | background:var(--clr3); 125 | padding:5px 10px; 126 | border-radius:10px; 127 | } 128 | 129 | /* cards */ 130 | 131 | .cards{ 132 | max-width:360px; 133 | height: 100%; 134 | display:grid; 135 | grid-template-columns: 320px; 136 | grid-template-rows: auto; 137 | margin: 20px auto; 138 | padding-bottom: 3%; 139 | align-items: baseline; 140 | justify-content: center; 141 | background:var(--clr5); 142 | grid-row-gap: 50px; 143 | 144 | 145 | } 146 | .card{ 147 | width:320px; 148 | height: auto; 149 | color:var(--clr2); 150 | position: relative; 151 | } 152 | .card img{ 153 | width:100%; 154 | height: 500px; 155 | } 156 | .overlay{ 157 | width: 100%; 158 | height: 500px; 159 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 160 | transition: all 0.3s cubic-bezier(.25,.8,.25,1); 161 | position: relative; 162 | margin-bottom: 20px; 163 | } 164 | .overlay:hover { 165 | box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); 166 | } 167 | 168 | .strip{ 169 | height: 80px; 170 | width:100%; 171 | /* background: var(--clr4); */ 172 | background:url('../strip.png'); 173 | position: absolute; 174 | background-position: center; 175 | 176 | left:0; 177 | bottom:0; 178 | } 179 | .strip .like-menu { 180 | background: transparent; 181 | display:flex; 182 | justify-content: center; 183 | height: 100px; 184 | } 185 | .cards-title{ 186 | max-width:320px; 187 | margin:40px auto; 188 | font-size: 3rem; 189 | color:var(--clr2); 190 | 191 | } 192 | .cards-title::before{ 193 | content: '\f005 '; 194 | font-family: "FontAwesome"; 195 | 196 | 197 | } 198 | .fa-2x{ 199 | color:var(--clr2); 200 | } 201 | .fa-spinner{ 202 | position: fixed; 203 | top:45vh; 204 | left:0; 205 | transform: translateX(49%); 206 | } 207 | .top-bar{ 208 | position: absolute; 209 | top:20px; 210 | right:30px; 211 | color:var(--clr1); 212 | z-index:9999; 213 | cursor: pointer; 214 | 215 | } 216 | .search{ 217 | height: 100%; 218 | padding-top:20%; 219 | display:flex; 220 | margin:0 auto; 221 | flex-direction: column; 222 | align-items: center; 223 | width:320px; 224 | 225 | } 226 | .search .like-menu{ 227 | background:var(--clr5); 228 | } 229 | .search-list{ 230 | display:flex; 231 | flex-direction: column; 232 | margin:20px auto; 233 | width:100%; 234 | align-items: center; 235 | } 236 | .input{ 237 | width:320px; 238 | height: 50px; 239 | border:0; 240 | background:var(--clr4); 241 | font-size: 1.2em; 242 | color:var(--clr2); 243 | padding-left: 3%; 244 | box-sizing: border-box; 245 | font-weight: 300; 246 | 247 | } 248 | .item { 249 | display:grid; 250 | grid-template-columns: 3fr 2fr; 251 | grid-template-rows: auto; 252 | padding:20px 4%; 253 | width:100%; 254 | margin:5px 0; 255 | grid-column-gap: 5%; 256 | background: var(--clr4); 257 | color:var(--clr2); 258 | box-sizing: border-box; 259 | 260 | } 261 | .item-author{ 262 | font-size:0.9rem; 263 | } 264 | .item-title{ 265 | font-size:1.05rem; 266 | } 267 | .item:hover{ 268 | background: var(--clr3); 269 | } 270 | /* menu bar */ 271 | .div{ 272 | height: 100%; 273 | } 274 | .single-card{ 275 | background:var(--clr4); 276 | display:grid; 277 | align-content: center; 278 | grid-template-columns:1fr; 279 | grid-template-rows: 600px 1fr 80px; 280 | grid-row-gap: 40px; 281 | 282 | 283 | } 284 | .single-card h1{ 285 | color: var(--clr2); 286 | font-family:'Playfair Display'; 287 | font-size: 3.5rem; 288 | font-weight: 900; 289 | } 290 | .single-card p{ 291 | font-family: 'Segoe UI', 'Source Sans Pro', Tahoma, Geneva, Verdana, sans-serif; 292 | color: var(--clr1); 293 | font-weight: 300; 294 | font-size: 1.3rem; 295 | opacity:0.9; 296 | line-height: 36px; 297 | } 298 | .book{ 299 | background:var(--clr3); 300 | display:flex; 301 | justify-content: center; 302 | height: 600px; 303 | } 304 | .book img{ 305 | width:320px; 306 | height: 480px; 307 | padding:70px 0 0; 308 | 309 | 310 | } 311 | .book-text{ 312 | background:var(--clr4); 313 | width:320px; 314 | padding:20px 0; 315 | box-sizing: border-box; 316 | margin:0px auto; 317 | } 318 | .btn-back{ 319 | background:var(--clr5); 320 | padding:10px 20px; 321 | text-align: center; 322 | width: 120px; 323 | margin:0 auto; 324 | font-size:1.2rem; 325 | font-weight: 400; 326 | cursor: pointer; 327 | } 328 | .single-card-category{ 329 | background:var(--clr1); 330 | color:var(--clr4); 331 | text-align: center; 332 | display: flex; 333 | justify-content: center; 334 | align-items: center; 335 | font-size: 2rem; 336 | font-weight: 700; 337 | cursor: pointer; 338 | } 339 | .content{ 340 | height: 100%; 341 | max-width:1600px; 342 | background: var(--clr3); 343 | padding:10%; 344 | box-sizing: border-box; 345 | display:flex; 346 | justify-content: center; 347 | align-items: center; 348 | flex-direction: column; 349 | margin:0 auto; 350 | margin-bottom: 5%; 351 | } 352 | .content h1{ 353 | font-size:4.5rem; 354 | font-family:'Playfair Display'; 355 | } 356 | .content-body p{ 357 | font-family: 'Segoe UI', 'Source Sans Pro', Tahoma, Geneva, Verdana, sans-serif; 358 | line-height: 30px; 359 | font-size: 1.3rem; 360 | font-weight: 300; 361 | } 362 | .position{ 363 | position: relative; 364 | } 365 | .vote{ 366 | position: absolute; 367 | top:0; 368 | left:90px; 369 | } 370 | .reset .strip{ 371 | display:none 372 | } 373 | @media(min-width:660px){ 374 | 375 | .container { 376 | min-height:100%; 377 | 378 | width:100%; 379 | } 380 | .cards{ 381 | max-width:660px; 382 | display:grid; 383 | height: auto; 384 | grid-template-columns: 320px 320px; 385 | grid-column-gap: 10px; 386 | grid-template-rows: auto; 387 | margin: 20px auto; 388 | 389 | justify-content: center; 390 | background:var(--clr5); 391 | } 392 | .cards-title{ 393 | max-width:680px; 394 | padding-left:20px; 395 | } 396 | .main-slide p{ 397 | font-size:2.2rem; 398 | max-width: 60%; 399 | 400 | } 401 | .search{ 402 | height: 100%; 403 | padding-top:10%; 404 | display:flex; 405 | margin:0 auto; 406 | flex-direction: column; 407 | align-items: center; 408 | width:640px; 409 | } 410 | .input{ 411 | width:640px; 412 | height: 70px; 413 | font-size: 1.3em; 414 | } 415 | 416 | .single-card{ 417 | grid-template-rows: 800px 1fr 80px; 418 | 419 | } 420 | .single-card h1{ 421 | color: var(--clr2); 422 | font-family:'Playfair Display'; 423 | font-size: 4.5rem; 424 | font-weight: 900; 425 | } 426 | .single-card p{ 427 | font-size: 1.5rem; 428 | } 429 | .book{ 430 | height: 800px; 431 | } 432 | .book img{ 433 | width:448px; 434 | height: 700px; 435 | 436 | } 437 | .book-text{ 438 | width:550px; 439 | } 440 | 441 | 442 | 443 | 444 | } 445 | 446 | @media(min-width:980px){ 447 | 448 | .cards{ 449 | max-width:960px; 450 | display:grid; 451 | height: auto; 452 | grid-template-columns: 320px 320px 320px; 453 | grid-template-rows: auto; 454 | margin: 20px auto; 455 | justify-content: center; 456 | background:var(--clr5); 457 | } 458 | .cards-title{ 459 | max-width:1000px; 460 | } 461 | .main-slide h1{ 462 | font-size:8rem; 463 | } 464 | .main-slide p{ 465 | font-size:1.8rem; 466 | max-width: 60%; 467 | 468 | } 469 | .single-card h1{ 470 | color: var(--clr2); 471 | font-family:'Playfair Display'; 472 | font-size: 2.7rem; 473 | font-weight: 900; 474 | margin-bottom: 10px; 475 | max-height: 170px; 476 | overflow: hidden; 477 | } 478 | .single-card .text{ 479 | overflow-y: scroll; 480 | margin-bottom: 30px; 481 | height: 280px; 482 | } 483 | .text p{ 484 | padding:10px 20px 0 0; 485 | font-size: 1.1rem; 486 | font-family: 'Segoe UI', 'Source Sans Pro', Tahoma, Geneva, Verdana, sans-serif; 487 | color: var(--clr1); 488 | opacity:0.9; 489 | line-height: 36px; 490 | } 491 | 492 | .div{ 493 | display:flex; 494 | justify-content: center; 495 | align-items: flex-start; 496 | padding-top: 80px; 497 | margin: 0 auto; 498 | width:100%; 499 | min-height:100%; 500 | padding-bottom: 80px; 501 | 502 | background-repeat: no-repeat; 503 | background-position: top center; 504 | } 505 | .single-card{ 506 | display:grid; 507 | grid-template-columns: 70px 450px 450px; 508 | grid-template-rows: 600px; 509 | align-content: center; 510 | grid-gap:10px; 511 | background: transparent; 512 | background:var(--clr4); 513 | position: relative; 514 | } 515 | .book{ 516 | background:transparent; 517 | display:flex; 518 | justify-content: center; 519 | height: 550px; 520 | width:100%; 521 | position: relative; 522 | } 523 | .book img{ 524 | width:440px; 525 | height: 680px; 526 | padding:0px 0 0; 527 | position: absolute; 528 | top:-40px; 529 | 530 | } 531 | .book-text{ 532 | background:transparent; 533 | height: 600px; 534 | padding: 40px 20px; 535 | width:450px; 536 | box-sizing: border-box; 537 | margin:0px auto; 538 | 539 | 540 | } 541 | .single-card-category{ 542 | grid-column-start: 1; 543 | grid-column-end: 2; 544 | grid-row-start: 1; 545 | grid-row-end: 2; 546 | height: 580px; 547 | margin-top: 10px; 548 | width:65px; 549 | margin-left:10px; 550 | background:var(--clr3); 551 | color:var(--clr1); 552 | word-break: break-all; 553 | font-size:2.3rem; 554 | text-transform: uppercase; 555 | 556 | } 557 | .btn-back{ 558 | position: absolute; 559 | bottom:30px; 560 | right: 30px; 561 | } 562 | .ctg{ 563 | writing-mode: vertical-rl; 564 | text-orientation: mixed; 565 | } 566 | 567 | 568 | } 569 | 570 | @media(min-width:1340px){ 571 | 572 | .cards{ 573 | max-width:1290px; 574 | display:grid; 575 | height: auto; 576 | grid-template-columns: 320px 320px 320px 320px; 577 | grid-template-rows: auto; 578 | margin: 20px auto; 579 | justify-content: center; 580 | background:var(--clr5); 581 | } 582 | .cards-title{ 583 | max-width:1340px; 584 | 585 | } 586 | .main-slide h1{ 587 | font-size:10rem; 588 | } 589 | .main-slide p{ 590 | font-size:2.2rem; 591 | max-width: 40%; 592 | 593 | 594 | } 595 | 596 | .single-card h1{ 597 | color: var(--clr2); 598 | font-family:'Playfair Display'; 599 | font-size: 3.5rem; 600 | font-weight: 900; 601 | margin-bottom: 40px; 602 | } 603 | .single-card .text{ 604 | overflow-y: scroll; 605 | margin-bottom: 40px; 606 | max-height: 400px; 607 | } 608 | .text p{ 609 | padding:10px 30px 10px 10px; 610 | font-size: 1.3rem; 611 | font-family: 'Segoe UI', 'Source Sans Pro', Tahoma, Geneva, Verdana, sans-serif; 612 | color: var(--clr1); 613 | opacity:0.9; 614 | line-height: 36px; 615 | 616 | } 617 | .div{ 618 | display:flex; 619 | justify-content: center; 620 | align-items: flex-start; 621 | padding-top: 150px; 622 | margin: 0 auto; 623 | width:100%; 624 | min-height:100%; 625 | padding-bottom: 150px; 626 | } 627 | .single-card{ 628 | display:grid; 629 | grid-template-columns: 80px 600px 650px; 630 | grid-template-rows: 700px; 631 | align-content: center; 632 | grid-gap:10px; 633 | background: transparent; 634 | background:var(--clr4); 635 | 636 | } 637 | .book{ 638 | background:transparent; 639 | display:flex; 640 | justify-content: center; 641 | height: 600px; 642 | width:100%; 643 | position: relative; 644 | } 645 | .book img{ 646 | width:600px; 647 | height: 900px; 648 | padding:0px 0 0; 649 | position: absolute; 650 | top:-100px; 651 | } 652 | .book-text{ 653 | background:transparent; 654 | height: 700px; 655 | padding: 40px; 656 | width:650px; 657 | box-sizing: border-box; 658 | margin:0px auto; 659 | position: relative; 660 | } 661 | .single-card-category{ 662 | grid-column-start: 1; 663 | grid-column-end: 2; 664 | grid-row-start: 1; 665 | grid-row-end: 2; 666 | height: 680px; 667 | margin-top: 10px; 668 | width:70px; 669 | margin-left:10px; 670 | word-break: break-all; 671 | font-size:3rem; 672 | 673 | } 674 | .btn-back{ 675 | position: absolute; 676 | bottom:50px; 677 | right: 50px; 678 | } 679 | .ctg{ 680 | writing-mode: vertical-rl; 681 | text-orientation: mixed; 682 | } 683 | 684 | 685 | 686 | } 687 | 688 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import * as serviceWorker from './serviceWorker'; 4 | import { createStore, applyMiddleware } from 'redux'; 5 | import { Provider } from 'react-redux'; 6 | import thunk from 'redux-thunk'; 7 | 8 | import reducer from './store/reducers/rootReducer'; 9 | import './css/index.css'; 10 | import App from './App'; 11 | 12 | 13 | const store = createStore(reducer, applyMiddleware(thunk)); 14 | 15 | ReactDOM.render(, document.getElementById('root')); 16 | 17 | 18 | // If you want your app to work offline and load faster, you can change 19 | // unregister() to register() below. Note this comes with some pitfalls. 20 | // Learn more about service workers: http://bit.ly/CRA-PWA 21 | serviceWorker.register(); 22 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/store/actions/BooksAction.js: -------------------------------------------------------------------------------- 1 | import config from '../../config' 2 | export const FetchApiBooks = () => { 3 | return (dispatch) => { 4 | console.log(process.env) 5 | fetch(`https://api.cosmicjs.com/v1/${config.COSMIC_BUCKET ? config.COSMIC_BUCKET : 'book-app'}?pretty=true&hide_metafields=true&read_key=${config.COSMIC_READ_KEY}`) 6 | .then(response => response.json()) 7 | .then(data => { 8 | console.log("Action creator from:", data); 9 | dispatch({type:"FETCH_BOOKS", data:data}); 10 | }) 11 | } 12 | } -------------------------------------------------------------------------------- /src/store/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | 2 | const initialState = { 3 | books: [], 4 | menu: [], 5 | value: '', 6 | search:[], 7 | author:[], 8 | likes: [], 9 | add:[] 10 | 11 | } 12 | 13 | const reducer = (state=initialState, action) => { 14 | switch(action.type) { 15 | case 'FETCH_BOOKS': 16 | 17 | return{ 18 | ...state, 19 | books: action.data.bucket.objects.filter(item => { 20 | return(item.type_slug === "books") 21 | }), 22 | menu: action.data.bucket.objects.filter(item => { 23 | return(item.type_slug === "categories") 24 | }), 25 | author: action.data.bucket.objects.filter(item => { 26 | return(item.type_slug === "authors") 27 | }) 28 | 29 | } 30 | case 'INPUT_CHANGE': 31 | console.log("Input", action); 32 | let newList = state.books.filter(item => { 33 | return item.slug.toLowerCase().search(action.input.toLowerCase()) !== -1 || item.metadata.author.title.toLowerCase().search(action.input.toLowerCase()) !== -1; 34 | 35 | }); 36 | 37 | return{ 38 | ...state, 39 | search: newList, 40 | value:action.input 41 | } 42 | case 'HANDLE_LIKE': 43 | 44 | return{ 45 | ...state, 46 | likes: state.books[action.like].options.slug_field ++ 47 | } 48 | 49 | default: 50 | return state; 51 | 52 | } 53 | 54 | } 55 | export default reducer; -------------------------------------------------------------------------------- /src/strip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-book-app/2f415225e775fb722b69251da350ee5701c3c9fc/src/strip.png --------------------------------------------------------------------------------