├── .babelrc ├── .gitignore ├── README.md ├── lib ├── app.js └── config │ └── index.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── App.js │ ├── actions │ │ ├── index.js │ │ └── slug.js │ ├── components │ │ ├── contentSlug.js │ │ ├── footer.js │ │ ├── headerSlug.js │ │ ├── logo.js │ │ ├── postItem.js │ │ └── postList.js │ ├── containers │ │ ├── Post.js │ │ └── Slug.js │ ├── index.js │ ├── logo.svg │ ├── reducers │ │ ├── index.js │ │ ├── posts.js │ │ └── slug.js │ ├── scss │ │ ├── app.scss │ │ ├── index.scss │ │ ├── posts.scss │ │ └── slug.scss │ ├── store.js │ └── test │ │ └── helpers │ │ ├── enzyme.js │ │ └── index.js └── public │ ├── favicon.ico │ └── index.html └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets" : ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Server 2 | node_modules 3 | npm-debug.log* 4 | /dist 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cosmic JS Blog 2 | ## React, Redux, Node, Cosmic JS 3 | 4 | 5 | 1. [React](#react) 6 | 1. [Node](#node) 7 | 1. [Config](#config) 8 | 1. [Development](#development) 9 | 1. [Demo](#demo) 10 | 11 | 12 | 13 | ## Node 14 | - ``` lib/app.js``` 15 | - Node REST API to query Cosmic JS DB 16 | - [cosmicjs-node](https://github.com/cosmicjs/cosmicjs-node) 17 | ``` javascript 18 | const params = { 19 | query: { 20 | type: 'posts', 21 | locale: 'en' // optional, if localization set on Objects 22 | }, 23 | limit: 5, 24 | props: 'id,slug,title,content', // get only what you need 25 | sort: '-created_at' // optional, defaults to order in dashboard 26 | } 27 | bucket.getObjects(params).then(data => { 28 | console.log(data) 29 | }).catch(err => { 30 | console.log(err) 31 | }) 32 | ``` 33 | 34 | ## React 35 | - ```src/app/*``` 36 | - Redux calls our Node API server, which in turn retrieves our Cosmic JS blog content. 37 | 38 | ## Development 39 | - ```npm install``` 40 | - Terminal 1 - ```npm run start:server``` 41 | - Terminal 2 - ```npm run start:dev``` 42 | - [localhost:3000]('http://localhost:3000') 43 | 44 | ## Config 45 | - Add the following code to your ```config/index.js``` file with your CosmicJS credentials. 46 | ``` javascript 47 | const config = { 48 | app: { 49 | port: process.env.PORT || 5000 50 | }, 51 | bucket : { 52 | slug: process.env.SLUG || 'YOUR COSMIC SLUG', 53 | write_key: process.env.WRITE_KEY ||'YOUR COSMIC WRITE KEY', 54 | read_key: process.env.READ_KEY ||' YOUR COSMIC READ KEY' 55 | } 56 | } 57 | 58 | module.exports = config; 59 | ``` 60 | 61 | 62 | ## [Demo](https://cosmicjs.com/apps/react-blog) 63 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import fs from 'fs' 3 | import path from 'path' 4 | import Cosmic from 'cosmicjs' 5 | import http_module from 'http' 6 | import cors from 'cors' 7 | import _ from 'lodash' 8 | 9 | const app = express(); 10 | 11 | 12 | app.use(cors()) 13 | app.set('port', (process.env.PORT || 5000)) 14 | const http = http_module.Server(app) 15 | 16 | 17 | //CosmicJS credentials 18 | const api = Cosmic(); 19 | const bucket = api.bucket({ 20 | slug: process.env.COSMIC_BUCKET || '15b45be0-f24b-11e7-8739-bd1a2fa1284e', 21 | read_key: process.env.COSMIC_READ_KEY || '', 22 | write_key: process.env.COSMIC_WRITE_KEY || '', 23 | }) 24 | 25 | app.get('/api/posts', (req, res) => { 26 | bucket.getObjects({ 27 | query: { 28 | type: 'posts' 29 | }}).then(data => { 30 | const posts = data.objects; 31 | res.send(posts); 32 | }).catch(err => { 33 | console.log(err) 34 | }) 35 | }) 36 | 37 | 38 | app.get('/api/posts/:slug', (req, res) => { 39 | bucket.getObject({ 40 | id: req.params.slug, // Object ID 41 | }).then(data => { 42 | const queryPost = data.object; 43 | res.send(queryPost); 44 | }).catch(err => { 45 | console.log(err) 46 | }) 47 | }) 48 | 49 | 50 | // if production, serve react bundle 51 | if(app.get('env') === 'production'){ 52 | app.use(express.static(path.resolve(__dirname, '../dist/build'))); 53 | app.get('*', (req, res) => { 54 | res.sendFile(path.resolve(__dirname, '../dist/build/index.html')) 55 | }) 56 | } 57 | 58 | 59 | http.listen(app.get('port'), () => { 60 | console.info('==> 🌎 Go to http://localhost:%s', app.get('port')); 61 | }) 62 | -------------------------------------------------------------------------------- /lib/config/index.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | app: { 3 | port: process.env.PORT || 5000 4 | }, 5 | bucket : { 6 | slug: process.env.SLUG || 'YOUR COSMIC SLUG', 7 | write_key: process.env.WRITE_KEY ||'YOUR COSMIC WRITE KEY', 8 | read_key: process.env.READ_KEY ||' YOUR COSMIC READ KEY' 9 | } 10 | } 11 | 12 | module.exports = config; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cosmic-blog", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "npm run clean & ./node_modules/.bin/webpack & npm run build:server & cross-env NODE_ENV=production npm run serve", 8 | "start:server": "node ./node_modules/nodemon/bin/nodemon.js lib/app.js --exec babel-node --presets @babel/preset-env", 9 | "start:client": "./node_modules/.bin/webpack-dev-server --port=3000 --mode development", 10 | "serve": "node ./dist/app.js", 11 | "clean": "rm -rf dist && mkdir dist", 12 | "test": "mocha --compilers js:babel-core/register --require ./app/src/test/helpers --require ./app/src/test/helpers/enzyme.js ./app/src/test/*.spec.js", 13 | "build:server": "babel lib -d dist", 14 | "build:client": "./node_modules/.bin/webpack", 15 | "build": "npm-run-all build:*", 16 | "production": "npm run build && cross-env NODE_ENV=production npm run serve" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "babel-loader": "^8.2.3", 22 | "babel-node": "0.0.1-security", 23 | "babel-preset-env": "^1.7.0", 24 | "babel-register": "^6.26.0", 25 | "body-parser": "1.19.0", 26 | "chai": "^4.3.4", 27 | "connected-react-router": "^6.9.1", 28 | "cookie-parser": "1.4.5", 29 | "cors": "2.8.5", 30 | "cosmicjs": "4.1.2", 31 | "cross-env": "^7.0.3", 32 | "css-loader": "^6.5.1", 33 | "enzyme": "^3.11.0", 34 | "enzyme-adapter-react-16": "^1.15.6", 35 | "express": "4.17.1", 36 | "extract-text-webpack-plugin": "^3.0.2", 37 | "file-loader": "^6.2.0", 38 | "history": "4.7.2", 39 | "html-webpack-plugin": "^5.5.0", 40 | "jsdom": "^18.1.0", 41 | "jsdom-global": "^3.0.2", 42 | "lodash": "4.17.21", 43 | "mini-css-extract-plugin": "^2.4.4", 44 | "mocha": "^9.1.3", 45 | "moment": "2.29.1", 46 | "node-sass": "6.0.1", 47 | "nodemon": "^2.0.15", 48 | "npm-run-all": "^4.1.5", 49 | "prop-types": "15.7.2", 50 | "react": "^17.0.2", 51 | "react-dom": "^17.0.2", 52 | "react-redux": "7.2.6", 53 | "react-router": "4.3.1", 54 | "react-router-dom": "6.0.2", 55 | "react-router-redux": "4.0.8", 56 | "redux": "4.1.2", 57 | "redux-logger": "3.0.6", 58 | "redux-thunk": "2.4.0", 59 | "sass-loader": "12.3.0", 60 | "sinon": "^12.0.1", 61 | "webpack": "^5.64.1", 62 | "webpack-dev-server": "^4.5.0" 63 | }, 64 | "devDependencies": { 65 | "@babel/cli": "^7.16.0", 66 | "@babel/core": "^7.16.0", 67 | "@babel/node": "^7.16.0", 68 | "@babel/preset-env": "^7.16.0", 69 | "@babel/preset-react": "^7.16.0", 70 | "html-loader": "^3.0.1", 71 | "webpack-cli": "^4.9.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/app/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Route, Link, Routes, BrowserRouter } from 'react-router-dom' 3 | import Post from './containers/Post' 4 | import Slug from './containers/Slug' 5 | 6 | 7 | const RouteList = () => ( 8 |
9 |
10 | 11 | 12 | } /> 13 | } /> 14 | 15 | 16 |
17 |
18 | ) 19 | 20 | export default RouteList 21 | -------------------------------------------------------------------------------- /src/app/actions/index.js: -------------------------------------------------------------------------------- 1 | //import fetch from 'cross-fetch'; 2 | 3 | export const RECEIVE_POSTS = 'RECEIVE_POSTS'; 4 | export const REQUEST_POSTS = 'REQUEST_POSTS'; 5 | 6 | function requestPosts(posts){ 7 | return { 8 | type: 'REQUEST_POSTS', 9 | posts 10 | } 11 | } 12 | 13 | 14 | function receivePosts(json){ 15 | return { 16 | type: 'RECEIVE_POSTS', 17 | posts: json.map(data => { 18 | data.author = data.metadata.author.title; 19 | data.authorImage = data.metadata.author.metadata.image.url; 20 | return data 21 | }), 22 | receivedAt: Date.now() 23 | } 24 | } 25 | 26 | 27 | export function postsHasErrored(bool){ 28 | return { 29 | type: 'POSTS_HAS_ERRORED', 30 | hasErrored: bool 31 | } 32 | } 33 | 34 | 35 | export function fetchBlogPosts(url) { 36 | return (dispatch) => { 37 | dispatch(requestPosts(url)) 38 | fetch(url) 39 | .then( response => response.json(), 40 | error => { 41 | console.log('An error occurred.', error) 42 | dispatch(postsHasErrored(error)) 43 | } 44 | ) 45 | .then(json => { 46 | console.log(json); 47 | dispatch(receivePosts(json)) 48 | } 49 | ) 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/app/actions/slug.js: -------------------------------------------------------------------------------- 1 | export const RECEIVE_SLUG = 'RECEIVE_SLUG' 2 | export const REQUEST_SLUG = 'REQUEST_SLUG' 3 | 4 | 5 | function requestSlug(slug){ 6 | return { 7 | type: 'REQUEST_SLUG', 8 | slug 9 | } 10 | } 11 | 12 | function receiveSlug(json){ 13 | const slug = { 14 | 'author': json.metadata.author.title, 15 | 'author_image': json.metadata.author.metadata.image.url, 16 | 'image': json.metadata.hero.url 17 | } 18 | return { 19 | type: 'RECEIVE_SLUG', 20 | slug: Object.assign(slug, json), 21 | receivedAt: Date.now() 22 | } 23 | } 24 | 25 | 26 | function slugHasErrored(bool){ 27 | return { 28 | type: 'SLUG_HAS_ERRORED', 29 | hasErrored: bool 30 | } 31 | } 32 | 33 | export function fetchSlugPost(url) { 34 | return (dispatch) => { 35 | dispatch(requestSlug(url)) 36 | fetch(url) 37 | .then( response => response.json(), 38 | error => { 39 | console.log('An error occurred.', error) 40 | dispatch(slugHasErrored(error)) 41 | } 42 | ) 43 | .then(json => { 44 | dispatch(receiveSlug(json)) 45 | } 46 | ) 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/app/components/contentSlug.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | const Content = ({post}) => { 5 | const htmlString = {post} 6 | return ( 7 |
8 |
9 |

10 |

11 | 12 | Back 13 | 14 |

15 |
16 |
17 | ) 18 | } 19 | 20 | export default Content 21 | -------------------------------------------------------------------------------- /src/app/components/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Logo from './logo.js' 3 | 4 | 5 | const Footer = () => ( 6 |
7 | 8 | Proudly powered by Cosmic JS 9 |
10 | ) 11 | 12 | export default Footer 13 | -------------------------------------------------------------------------------- /src/app/components/headerSlug.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Header = ({title, author, date, authorImage, imageUrl}) => ( 4 |
5 |
6 |

Cosmic JS

7 |

{title}

8 |
9 |
10 | By {author} on {date} 11 |
12 |
13 |
14 | ) 15 | 16 | 17 | export default Header 18 | -------------------------------------------------------------------------------- /src/app/components/logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import icon from '../logo.svg' 3 | 4 | const Logo = ({logo, height, width, id, styleName}) => ( 5 |
6 | logo 13 |
14 | ) 15 | 16 | export default Logo 17 | -------------------------------------------------------------------------------- /src/app/components/postItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import PropTypes from 'prop-types' 4 | import moment from 'moment' 5 | 6 | 7 | const Post = ({ completed, image, title, slug, author, date, authorImage,pid}) => { 8 | return ( 9 | 10 |
11 |
12 | 13 |
14 |
15 |

16 | {title} 17 |

18 |
19 |
20 |
21 | 22 | {author} 23 |
24 |

{date}

25 |
26 |
27 | 28 | ) 29 | } 30 | 31 | Post.propTypes ={ 32 | image: PropTypes.string.isRequired, 33 | title: PropTypes.string.isRequired, 34 | date: PropTypes.string.isRequired, 35 | slug: PropTypes.string.isRequired, 36 | author: PropTypes.string.isRequired, 37 | authorImage: PropTypes.string.isRequired 38 | } 39 | 40 | export default Post 41 | -------------------------------------------------------------------------------- /src/app/components/postList.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import PropTypes from 'prop-types' 3 | import PostComponent from '../components/postItem' 4 | import moment from 'moment' 5 | 6 | const PostList = ({posts}) => ( 7 | 21 | ) 22 | 23 | 24 | PostList.propTypes = { 25 | posts: PropTypes.arrayOf( 26 | PropTypes.shape({ 27 | title: PropTypes.string.isRequired, 28 | date: PropTypes.instanceOf(Date), 29 | slug: PropTypes.string.isRequired, 30 | author: PropTypes.string.isRequired, 31 | authorImage: PropTypes.string.isRequired, 32 | }).isRequired 33 | ).isRequired 34 | } 35 | 36 | export default PostList 37 | -------------------------------------------------------------------------------- /src/app/containers/Post.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PostList from '../components/postList' 3 | import { connect } from 'react-redux' 4 | import { togglePost, fetchBlogPosts } from '../actions' 5 | import { fetchSlugPost } from '../actions/slug' 6 | import PropTypes from 'prop-types' 7 | import Footer from '../components/footer' 8 | import Logo from '../components/logo' 9 | import '../scss/index.scss' 10 | 11 | class Post extends Component { 12 | 13 | componentDidMount(){ 14 | this.props.fetchData(window.location.href + 'api/posts') 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | 22 |

Cosmic JS

23 |
24 | 27 |
29 | ); 30 | } 31 | } 32 | 33 | const mapStateToProps = (state) => { 34 | return { 35 | data: state.posts.posts 36 | } 37 | } 38 | 39 | const mapDispatchToProps = (dispatch) => { 40 | return { 41 | fetchData: (url) => dispatch(fetchBlogPosts(url)) 42 | } 43 | } 44 | 45 | 46 | export default connect(mapStateToProps, mapDispatchToProps)(Post) 47 | -------------------------------------------------------------------------------- /src/app/containers/Slug.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { fetchSlugPost } from '../actions/slug' 4 | import Content from '../components/contentSlug' 5 | import Header from '../components/headerSlug' 6 | import Footer from '../components/footer' 7 | import moment from 'moment'; 8 | 9 | class Slug extends Component { 10 | componentDidMount(){ 11 | this.props.fetchSlug(window.location.origin + '/api/posts' + window.location.pathname); 12 | } 13 | 14 | render(){ 15 | return( 16 |
17 |
24 | 27 |
29 | ) 30 | } 31 | } 32 | 33 | 34 | const mapStateToProps = (state) => { 35 | return { 36 | slug: state.slug.slug 37 | } 38 | } 39 | 40 | const mapDispatchToProps = (dispatch) => { 41 | return { 42 | fetchSlug: (url) => dispatch(fetchSlugPost(url)) 43 | } 44 | } 45 | 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(Slug) 48 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Provider } from 'react-redux' 4 | import { ConnectedRouter } from 'connected-react-router' 5 | import configureStore, { history } from './store' 6 | import App from './App.js' 7 | 8 | 9 | const store = configureStore() 10 | 11 | render( 12 | 13 | 14 |
15 | 16 |
17 |
18 |
, 19 | document.getElementById('root') 20 | ) 21 | -------------------------------------------------------------------------------- /src/app/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { connectRouter } from 'connected-react-router' 3 | import posts from './posts' 4 | import slug from './slug' 5 | 6 | const cosmicBlog = (history) => combineReducers({ 7 | posts, 8 | slug, 9 | router: connectRouter(history), 10 | }) 11 | 12 | export default cosmicBlog 13 | -------------------------------------------------------------------------------- /src/app/reducers/posts.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_POSTS, REQUEST_POSTS} from '../actions' 2 | 3 | const posts = (state = { 4 | isFetching: false, 5 | posts: [] 6 | }, action) => { 7 | switch(action.type){ 8 | case 'REQUEST_POSTS': 9 | return Object.assign({}, state, { 10 | isFetching: true, 11 | }) 12 | case 'RECEIVE_POSTS': 13 | return Object.assign({}, state, { 14 | isFetching: false, 15 | posts: action.posts 16 | }) 17 | default: 18 | return state 19 | } 20 | } 21 | 22 | export default posts 23 | -------------------------------------------------------------------------------- /src/app/reducers/slug.js: -------------------------------------------------------------------------------- 1 | import {RECEIVE_SLUG, REQUEST_SLUG} from '../actions/slug' 2 | 3 | const Slug = (state = { 4 | isFetching: false, 5 | slug: [] 6 | }, action) => { 7 | switch(action.type){ 8 | case 'REQUEST_SLUG': 9 | return Object.assign({}, state, { 10 | isFetching: true, 11 | }) 12 | case 'RECEIVE_SLUG': 13 | return Object.assign({}, state, { 14 | isFetching: false, 15 | slug: action.slug 16 | }) 17 | default: 18 | return state 19 | } 20 | } 21 | 22 | export default Slug 23 | -------------------------------------------------------------------------------- /src/app/scss/app.scss: -------------------------------------------------------------------------------- 1 | html,body { 2 | margin: 0; 3 | padding: 0; 4 | height: 100%; 5 | font-family: 'Josefin Sans', sans-serif; 6 | 7 | } 8 | 9 | .footer { 10 | position: absolute; 11 | width: 100%; 12 | padding: 40px; 13 | } 14 | 15 | .logoFooter { 16 | float: left; 17 | } 18 | 19 | .footer > span { 20 | padding-top: 15px; 21 | float: left; 22 | } 23 | 24 | 25 | .App-logo { 26 | animation: App-logo-spin infinite 20s linear; 27 | height: 80px; 28 | } 29 | 30 | .App-header { 31 | background-color: $backgroundColor; 32 | height: $headerMinHeight; 33 | color: white; 34 | text-align: center; 35 | margin-bottom: -142px; 36 | padding-top: 40px; 37 | } 38 | 39 | .App-title { 40 | font-size: 1.5em; 41 | } 42 | 43 | .App-intro { 44 | font-size: large; 45 | } 46 | 47 | @keyframes App-logo-spin { 48 | from { transform: rotate(0deg); } 49 | to { transform: rotate(360deg); } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/scss/index.scss: -------------------------------------------------------------------------------- 1 | $headerMinHeight: 300px; 2 | $headerMaxHeight: 500px; 3 | $backgroundColor: #222; 4 | 5 | @import 'posts'; 6 | @import 'slug'; 7 | @import 'app'; 8 | -------------------------------------------------------------------------------- /src/app/scss/posts.scss: -------------------------------------------------------------------------------- 1 | .postList{ 2 | overflow: hidden; 3 | } 4 | 5 | .cardContainer{ 6 | margin: 4% 1%; 7 | height: 400px; 8 | border-radius: 4px; 9 | position: relative; 10 | text-align: center; 11 | background: white; 12 | box-shadow: 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.3); 13 | float: left; 14 | font-family: 'Josefin Sans', sans-serif; 15 | color: $backgroundColor; 16 | transition: width 0.1s linear; 17 | } 18 | 19 | .article > div > img { 20 | width:30px; 21 | height:30px; 22 | border-radius:50%; 23 | background-size:cover; 24 | box-shadow:0 2px 3px rgba(0,0,0,0.3); 25 | } 26 | .postDate { 27 | @extend .right 28 | } 29 | 30 | .authorName { 31 | float: left; 32 | margin-left: 3%; 33 | transform-origin: left; 34 | } 35 | 36 | .article{ 37 | position: absolute; 38 | bottom: 0px; 39 | left: 0; 40 | right: 0; 41 | margin: auto 42 | } 43 | 44 | .card-desc-author { 45 | border: medium dashed green; 46 | position: relative; 47 | display:flex; 48 | bottom: 0; 49 | width: 90%; 50 | margin: 0% 2px; 51 | } 52 | 53 | @media only screen and (min-width: 1000px) { 54 | .cardContainer { 55 | width: 30%; 56 | margin: 1em; 57 | } 58 | 59 | .card-image{ 60 | max-height: 15%; 61 | } 62 | } 63 | 64 | @media only screen and (max-width: 999px) { 65 | .cardContainer { 66 | width: 96%; 67 | margin: 2em -.5em; 68 | margin-bottom: 80px; 69 | } 70 | .card-image{ 71 | max-height: 35%; 72 | } 73 | } 74 | 75 | .right { 76 | float: right; 77 | margin-right: 3%; 78 | transform-origin: right; 79 | } 80 | 81 | .expanded{ 82 | box-shadow: none; 83 | position: fixed; 84 | width: 100%; 85 | height: 100%; 86 | z-index: 99; 87 | margin:0px; 88 | transition: width 0.3s ease, height 0.5s 0.2s ease; 89 | } 90 | 91 | .card-image{ 92 | width: 100%; 93 | max-height: 200px; 94 | overflow: hidden; 95 | } 96 | 97 | .card-image img{ 98 | width: 100%; 99 | } 100 | 101 | .card-desc{ 102 | font-family: 'Source Serif Pro', serif; 103 | font-weight: 400; 104 | margin: 2% 1%; 105 |  Overflow: auto; 106 | } 107 | 108 | // card title 109 | .card-desc-head{ 110 | padding: 5vh; 111 | margin: auto; 112 | font-weight: 300; 113 | font-size: 25px; 114 | overflow: hidden; 115 | } 116 | 117 | 118 | 119 | @keyframes text-appear{ 120 | 0%{opacity: 0} 121 | 50%{opacity: 0; position: relative; top: 10px;} 122 | 100%{opacity: 1; position: relative; top: 0px;} 123 | } 124 | -------------------------------------------------------------------------------- /src/app/scss/slug.scss: -------------------------------------------------------------------------------- 1 | html,body { 2 | margin:0; 3 | height:120%; 4 | font-family: 'Josefin Sans', sans-serif; 5 | 6 | } 7 | 8 | .author{ 9 | display:inline-block; 10 | width:50px; 11 | height:50px; 12 | border-radius:50%; 13 | background-size:cover; 14 | box-shadow:0 2px 3px rgba(0,0,0,0.3); 15 | margin-bottom:3px 16 | } 17 | 18 | .header { 19 | position:relative; 20 | overflow:hidden; 21 | background-repeat: no-repeat; 22 | background-size: cover; 23 | display:flex; 24 | flex-wrap: wrap; 25 | justify-content: center; 26 | align-items: flex-start; 27 | align-content: flex-start; 28 | height:50vw; 29 | min-height:$headerMinHeight; 30 | max-height:$headerMaxHeight; 31 | min-width:300px; 32 | color:#eee; 33 | } 34 | 35 | .header a{ 36 | color:#eee 37 | } 38 | 39 | @media only screen and (min-width: 1000px) { 40 | .header { 41 | height:20vw; 42 | } 43 | .info > h1 { 44 | font-size: 50px; 45 | 46 | } 47 | .info > h4 > a { 48 | font-size: 20px; 49 | } 50 | .author { 51 | width:55px; 52 | height:55px; 53 | } 54 | } 55 | 56 | @media only screen and (max-width: 999px) { 57 | .header { 58 | height:10vw; 59 | } 60 | } 61 | 62 | .info{ 63 | width:100%; 64 | padding:0 10% 0 10%; 65 | text-align:center; 66 | text-shadow:0 2px 3px rgba(0,0,0,0.2) 67 | } 68 | 69 | 70 | .info h4, .meta{ 71 | font-size:0.7em 72 | } 73 | .meta{ 74 | font-style:italic; 75 | } 76 | @keyframes grow{ 77 | 0% { transform:scale(1)} 78 | 50% { transform:scale(1.2)} 79 | } 80 | .content{ 81 | padding:5% 10% 10%; 82 | text-align:justify; 83 | color: #2f2f2f; 84 | font-size: 17px; 85 | line-height: 26px; 86 | margin:10px 0; 87 | font-weight: 300; 88 | } 89 | 90 | .btn { 91 | color:#333; 92 | border:2px solid; 93 | border-radius:3px; 94 | text-decoration:none; 95 | display:inline-block; 96 | padding:5px 10px; 97 | margin: auto; 98 | font-weight:600; 99 | } 100 | -------------------------------------------------------------------------------- /src/app/store.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import thunk from 'redux-thunk' 3 | import { createBrowserHistory } from 'history' 4 | import { createStore, applyMiddleware, compose } from 'redux' 5 | import { routerMiddleware } from 'connected-react-router' 6 | import RootReducer from './reducers' 7 | 8 | export const history = createBrowserHistory() 9 | 10 | 11 | const initialState = {} 12 | 13 | 14 | export default function configureStore(initialState) { 15 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 16 | const store = createStore( 17 | RootReducer(history), 18 | composeEnhancer( 19 | applyMiddleware( 20 | thunk, 21 | routerMiddleware(history), 22 | ), 23 | ), 24 | ) 25 | 26 | // Hot reloading 27 | if (module.hot) { 28 | // Enable Webpack hot module replacement for reducers 29 | module.hot.accept('./reducers', () => { 30 | store.replaceReducer(RootReducer(history)); 31 | }); 32 | } 33 | 34 | return store; 35 | } -------------------------------------------------------------------------------- /src/app/test/helpers/enzyme.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/app/test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import 'jsdom-global/register'; 2 | import { expect } from 'chai'; 3 | import { sinon, spy } from 'sinon'; 4 | import { mount, render, shallow } from 'enzyme'; 5 | 6 | global.expect = expect; 7 | global.sinon = sinon; 8 | global.spy = spy; 9 | 10 | global.mount = mount; 11 | global.render = render; 12 | global.shallow = shallow; 13 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/react-blog/80ea31d81b818f493a8fdda9fe647299ec757f32/src/public/favicon.ico -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Cosmic JS Blog 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 7 | 8 | 9 | const paths = { 10 | BUILD: path.resolve(__dirname, 'dist/build'), 11 | SRC: path.resolve(__dirname, 'src/app'), 12 | PUBLIC: path.resolve(__dirname, 'src/public') 13 | } 14 | 15 | 16 | let config = { 17 | entry : path.join(paths.SRC, 'index.js'), 18 | output : { 19 | path: paths.BUILD, 20 | filename: 'bundle.js' 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(js|jsx)$/, 26 | exclude: /node_modules/, 27 | use: { 28 | loader: 'babel-loader', 29 | options: { 30 | presets: ['@babel/preset-env', '@babel/preset-react'] 31 | } 32 | } 33 | }, 34 | { 35 | test: /\.(css|scss)$/, 36 | use:[MiniCssExtractPlugin.loader,'css-loader', 'sass-loader'], 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/, 40 | use: [ 41 | 'file-loader', 42 | ], 43 | }, 44 | { 45 | test: /\.html$/, 46 | use: 'html-loader' 47 | } 48 | ], 49 | }, 50 | plugins : [ 51 | new HtmlWebpackPlugin({ 52 | template: path.join(paths.PUBLIC, 'index.html') 53 | }), 54 | new MiniCssExtractPlugin(), 55 | ], 56 | devServer: { 57 | port: 3000, 58 | proxy: { 59 | '/api' : 'http://localhost:5000', 60 | pathRewrite: {'^/api' : ''} 61 | }, 62 | historyApiFallback: true 63 | }, 64 | resolve: { 65 | extensions: ['.js', '.jsx'], 66 | }, 67 | devtool: 'source-map' 68 | } 69 | 70 | module.exports = config; 71 | --------------------------------------------------------------------------------