├── .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 |
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 |

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 |
8 | {posts.map((item, index) => (
9 |
19 | ))}
20 |
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 |
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 |
28 |
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 |
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 |
--------------------------------------------------------------------------------