Codementor Events

How to Build a React Portfolio Website Powered by Cosmic JS

Published Jan 07, 2019Last updated Jul 06, 2019
How to Build a React Portfolio Website Powered by Cosmic JS

In this tutorial I'm going to demonstrate how to build a photography portfolio website using React, Styled-Components and Cosmic JS. Let's get started. 

TL;DR

View and install the demo
View the code on GitHub

Intro

Cosmic JS provides a good backend for your webapps. It is a fully-featured content management system (CMS) with numerous options that allows everyone on your team to collaborate to manage content. Cosmic JS provides many options for developers: Bucket Admins can assign different roles for their team members, create single or multiple relationships between Objects and much more. 

In this example I use:

  • the multiple objects relationship metafield
  • images / files
  • default objects input

ReactJS: easy and flexible JS library to create a client side app. The React CLI allows you to create single page apps with no configuration.

StyledComponents: alternative for Static CSS, allows you to build separate components to enhance your app and make it expandable.

Getting Started

1. In your Cosmic JS dashboard click " Add New Bucket".

 1.1**  Install an App **- Install an App button, assign the application name and "Save Bucket".  After that,  select the application from the list of applications and click "Install free". And you created all the objects necessary for the application. The last thing to do: Copy the API key from the Cosmic JS desktop> Basic settings> Bucket slug.

 1.2  From Scratch -  start from scratch button, assign the application name and "Save bucket".  For this app, you need to create three objects type. Images, Categories and Sites. 

Images : Default inputs plus additional metafields called: "img"  as image/file  object type checked as required. 

Categories : Default inputs plus extra metafields called: "images" as multiple objects relationship (select limit Search by Images) and Image/file object called: "img". 

Sites : Default inputs type plus additional metafields called: "picture".

I've created the Bucket for my application, and am now ready to dive into the code.  Link for your Bucket Slug.

Code

Clone the repo from Github or you can create from scratch. This package shows all dependencies used for app:

{
  "name": "portfolio",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "cosmicjs": "^3.2.14",
    "react": "^16.6.3",
    "react-dom": "^16.6.3",
    "react-pose": "^4.0.4",
    "react-pose-text": "^3.1.0",
    "react-router-dom": "^4.3.1",
    "react-scripts": "2.1.1",
    "styled-components": "^4.1.3",
    "styled-loaders": "^0.3.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}

After you've installed React and all dependencies, you are ready to write code.

All data is downloaded separately, a parameter is sent in the react-router. After moving to a single post, a single element is fetch and rendered in component.

A brief explanation about the file structure.

  • Our first task is to create a file structure for the project. 

App.js default , folder component: 

buttons  (all created buttons),

headers  (all headers),

page  (all page),

parts  (all stateless  only const styled components),

section  (class and functional components that manage data and import other component from parts file),

l ayout  (app layout global style theme provider).

utils  (color and theme object).

  App.js
│   index.js
│   serviceWorker.js
│
├───components
│   ├───buttons
│   │       Button.js
│   │
│   ├───headers
│   │       H1.js
│   │       H2.js
│   │
│   ├───page
│   │       Contact.js
│   │
│   ├───parts
│   │       Anchor.js
│   │       Caption.js
│   │       Card.js
│   │       CategoryCard.js
│   │       Center.js
│   │       Contain.js
│   │       Figure.js
│   │       Paragraph.js
│   │       PositionContainer.js
│   │
│   └───section
│           Category.js
│           Footer.js
│           Nav.js
│           PartGrid.js
│           Single.js
│           Wrap.js
│
├───layout
│       Layout.js
│
└───utils
        colors.js
        theme.js

What's Inside

App.js  file that is located routing and layout import, state for interactive elements. Here we pass data that we can pick up in another component as props.

import React, { Component } from 'react';
import {BrowserRouter, Switch, Route} from 'react-router-dom';

//CSS COMPONENT IMPORTS
import './font-awesome-4.7.0/css/font-awesome.min.css';
import Layout from './layout/Layout';
import Wrap from './components/section/Wrap';
import Category from './components/section/Category';
import PartGrid from './components/section/PartGrid';
import Contact from './components/page/Contact';
import Single from './components/section/Single';
import Footer from './components/section/Footer';
import Center from './components/parts/Center';

class App extends Component {
  state = {
    category:null,
    hg:false
  }
  componentDidMount = async () => {
    const Cosmic = require('cosmicjs');
    const api = Cosmic();
    const bucket = api.bucket({
    slug: 'ec055990-f24c-11e8-9231-9b47e8f95b7e'
    })
    const data = await bucket.getObjects({
      type: 'categories'
    })
    this.setState({
      category: data.objects,
    })
    document.addEventListener('scroll', () => {
      if(window.pageYOffset > 50 ) {
        this.setState({
          hg: true
        })
        }
        else{
          this.setState({
            hg: false
          })
        }
    });  
}
  handleMenu = () => {
    this.setState((prevProps) => ({visable: !prevProps.visable }));
  }    
  render() {
    console.log(this.state.category);
    return (
    <div className="App">
    <Layout>
    <BrowserRouter>
    <>
      <Wrap hg={this.state.hg} />
      <Switch>
        <Route path="/" exact render={(props) => <Category category={this.state.category}/>}/>  
        <Route path='/contact' exact component={Contact}  />
        <Route path='/img/:slug' component={Single} exact  />
        <Route path='/:slug' component={PartGrid} exact  />
      </Switch>
      </>
     </BrowserRouter>
     <Center fs> " A portfolio is a set of pictures by someone, or photographs of examples of their work, which they use when entering competitions or applying for work. "</Center>
     <Footer/>
    </Layout>
     </div>
    );
  }
}

export default App;

Section 

Category.js  is rendering as a home page. This displays data downloaded as props from the app.js component.  It also imports styled components from the "parts" folder.

import React from 'react'
import CategoryCard from './../parts/CategoryCard';
import CaptionWrap from '../parts/Caption';
import Button from './../buttons/Button';
import H1 from '../headers/H1';
import ContainerCategory from './../parts/Contain';
import Position from './../parts/PositionContainer';
import { Link } from 'react-router-dom'
import Par from './../parts/Paragraph';

export default function Category(props) {
  console.log("Props category", props);
return (
<ContainerCategory>
{props.category && props.category.map((item, index) => {
  return(
    <CategoryCard key={index} index={index + 1}>
      <img src={item.metadata.img.url} alt="img" index={index + 1} />
        <CaptionWrap index={index + 1}>   
         <Position index={index + 1}>
            <H1 isBig>{item.title}</H1>
            <Par  dangerouslySetInnerHTML={{__html:item.content}}></Par>
            <Link to={'/' + item.slug}><Button >See more</Button> </Link>
         </Position>
        </CaptionWrap>
      </CategoryCard>
      )
    })}
    </ContainerCategory>
  )
}

PartGrid.js -  state component collects data from Cosmic JS. The page with photos in the selected category contains a link to a single photo.

import React, { Component } from 'react'
import styled from 'styled-components'

import Card from '../parts/Card';
import Img from '../parts/Figure';
import Anchor from './../parts/Anchor';
import { Link } from 'react-router-dom';


const GridContainer = styled.div`
    min-height:100%;
    display:grid;
    margin:150px auto;
    justify-items:center;
    grid-template-column:1fr;
    grid-template-rows:400px;
    grid-gap:10px;
${({theme}) => theme.media.mobile} {
   grid-template-columns:320px 320px;
   width:640px;
}
${({theme}) => theme.media.tablet} {
    grid-template-columns:320px 320px 320px;
    width:960px;
    
}
${({theme}) => theme.media.desktop} {
    grid-template-columns:320px 320px 320px 320px;
    width:1280px;
    margin:200px auto 450px auto;
}
`;
export default class PartGrid extends Component {
    state ={
        picture: []
    }
    componentDidMount = async() => {
        const slug = this.props.match.params.slug;

        const Cosmic = require('cosmicjs')
        const api = Cosmic()
        const bucket = api.bucket({
        slug: 'imageapp'
        })
        const data = await bucket.getObject({
        slug: `${slug}`
        })
        this.setState({
            picture:data.object
        })
    }
  render() {
    return (
        <GridContainer column={true}>
        { this.state.picture.metadata && this.state.picture.metadata.images.map((item, index) => {
        return(
          <Card key={index}>
            <Img src={item.metadata.img.url} alt="grid-img"/>
            <Anchor as={Link} to={'/img/' + item.slug}>
            <i className="fa fa-link" aria-hidden="true"></i>
            </Anchor>
          </Card>
        )
       })}    
        </GridContainer>
    )
  }
}

Single.js  - It is also a component containing state. Receives data for a single photo from Cosmic JS.

import React, { Component } from 'react'

import CategoryCard from './../parts/CategoryCard';
import CaptionWrap from '../parts/Caption';
import Button from './../buttons/Button';
import H1 from '../headers/H1';
import ContainerCategory from './../parts/Contain';
import Position from './../parts/PositionContainer';
import { Link } from 'react-router-dom';
import Par from './../parts/Paragraph';

export default class Single extends Component {
    state ={
        img: null
    }
    componentDidMount = async() => {
        const link = this.props.match.params.slug;

        const Cosmic = require('cosmicjs')
        const api = Cosmic()
        const bucket = api.bucket({
        slug: 'imageapp'
        })
        const data = await bucket.getObject({
        slug: `${link}`
        })
        this.setState({
            img:data.object
        })
    }
 
  render() {
    return (
    <ContainerCategory>
    <CategoryCard index={0}>
    {this.state.img && <img src={this.state.img.metadata.img.url} alt="img" index={0} /> }
        <CaptionWrap index={0}>   
         <Position index={0}>
         {this.state.img &&  <H1 isBig>{this.state.img.title}</H1>}
         {this.state.img &&  <Par  dangerouslySetInnerHTML={{__html:this.state.img.content}}></Par>}
         </Position>
        </CaptionWrap>
      </CategoryCard>
      <br/> <br/>
      <Link to={'/'}><Button >Go back</Button></Link>
    </ContainerCategory>
    )
  }
}

Layout.js -   contains theme provider and global style. Since we used the theme provider, we can connect CSS with React and use CSS variables depending on props from the component.

import React from 'react'

import {createGlobalStyle, ThemeProvider} from 'styled-components';
import { theme } from './../utils/theme';

const GlobalStyle = createGlobalStyle`
 body {
   padding: 0;
   box-sizing:border-box;
   margin 0;
   background:${({theme}) => theme.colors.light}
   font-family: Montserrat,'Segoe UI', sans-serif ;
   color:${({theme}) => theme.colors.dark}
  }
`;
export default function Layout({children}) {
  return (
    <ThemeProvider theme={theme}>
        <>
        <GlobalStyle />
        {children}
        </>
    </ThemeProvider>
  
  )
}

Nav.js - separate state component with own styled-components and state.

Parts 

In this folder there are only single tags HTML styling in CSS (styled-components) and exported as separate components.

Buttons and Headers

Individual HTML tags styling in styled-components): h1, button etc.

Page

Contact.js - State component that fetch data from Cosmic JS and display in app. 

Utils

All global variables that we can use in our app like colors, media query, font-size etc. Variables from the files we can pass as props to other components and use in styled-components.

export const colors = {

    dark: '#1B1B1E',
    darkOne: '#373F51',
    middle: '#373F51',
    // light: '#D8DBE2',
    lightOne: '#A9BCD0',
    light: 'white'
}
import {colors} from './colors';

export const theme = {
    colors,
    fontWg: {
        thin:300,
        reg:400,
        fat:800
    },
    media: {
        desktop: '@media(min-width: 1324px)',
        tablet: '@media(min-width: 1024px)',
        mobile: '@media(min-width: 665px)',
    }
}

Conclusion

We've just created a simple app using styled-components, React JS and Cosmic JS. At the beginning we saw how to create the Bucket for our app. We then created a boilerplate using the React CLI and installed dependencies. After creating the folder structure and styling components in CSS, we downloaded data from Cosmic JS to display in the application.

Cosmic JS has a great API for every application, coupled with many options for the developer and content editors that allow you to manage content seamlessly. Styled components allows you to create components that are independent, so an application can be created by many people without causing conflicts like (class name in css ) etc. In one command the React CLI allows us to create the boilerplate for our app. 

I encourage you to install the application, add your own components and extend the functionality. Have fun!

If you have any comments or questions about building apps with Cosmic JS, reach out to us on Twitter and join the conversation on Slack.

Discover and read more posts from Andrzej Ogorek
get started
post commentsBe the first to share your opinion
bougti
5 years ago

I’ve tried out several CMS’ of both standard and GraphQL and ones with plugins vs full-featured, and I’d recommend using Cosmic JS. It is one of the simpler to understand with a low learning curve, and one of the more pleasurable to use. The only advice I have is that you can’t use built in formatting, esp. for production apps. Instead, you should use plain text metafields and use as many as needed for separate sections for whatever object you need. Otherwise, you’ll have to parse it “dangerously”, meaning that there could be script tags injected (even though this is SUPER unlikely, it’s possible). However, that’s totally fine, work-aroundable, and by far worth it.
see: https://inro.in/lucky-patcher/ , https://inro.in/9apps/ & https://inro.in/vidmate/

Show more replies