├── .dockerignore ├── .gitignore ├── .babelrc ├── api_key.png ├── favicon.png ├── search.png ├── src ├── dispatchers │ └── Dispatcher.js ├── utils │ ├── format-date.js │ ├── getID.js │ ├── is-preview-set-in-query.js │ └── long-name-short.js ├── components │ ├── assets │ │ ├── AssetsContainer.css │ │ ├── Thumbnail.css │ │ ├── Search.css │ │ ├── Assets.css │ │ ├── Search.js │ │ ├── Assets.js │ │ ├── AssetContainer.js │ │ ├── Thumbnail.js │ │ ├── Asset.css │ │ ├── AssetsContainer.js │ │ └── Asset.js │ ├── requests │ │ ├── Request.css │ │ ├── RequestListItem.js │ │ ├── Requests.js │ │ └── Request.js │ ├── NoMatch.js │ ├── Error.js │ ├── NotificationLink.css │ ├── tabs │ │ ├── Pane.js │ │ ├── Tabs.css │ │ └── Tabs.js │ ├── content-types │ │ ├── ContentTypeListItem.js │ │ └── ContentTypesContainer.js │ ├── TwoPanelList.css │ ├── Main.js │ ├── List.js │ ├── NotificationLink.js │ ├── entries │ │ ├── EntryListItem.js │ │ ├── Field.css │ │ ├── EntryLinkContainer.js │ │ ├── Entry.js │ │ ├── EntriesContainer.js │ │ └── Field.js │ ├── List.css │ ├── Nav.js │ ├── TwoPanelList.js │ ├── App.css │ ├── ToggleButton.css │ ├── ToggleButton.js │ ├── App.js │ └── settings │ │ ├── Settings.css │ │ ├── SettingsContainer.js │ │ └── SettingsForm.js ├── actions │ ├── actions.js │ └── actionCreators.js ├── reducers │ ├── requests.js │ ├── index.js │ ├── contentTypes.js │ ├── api.js │ └── entries.js ├── services │ ├── contentfulClient.js │ ├── contentTypeStore.js │ └── entriesStore.js ├── store.js └── main.js ├── contentful_logo_120x90@2x.png ├── Dockerfile ├── main.css ├── .travis.yml ├── bin ├── analytics.json └── publish.sh ├── index.html ├── webpack.config.js ├── README.md ├── package.json └── contentful.svg /.dockerignore: -------------------------------------------------------------------------------- 1 | # prevent copying the host node_modules 2 | node_modules 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | gh-page 4 | *.swp 5 | *.swo 6 | *.log 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /api_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/discovery-app-react/HEAD/api_key.png -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/discovery-app-react/HEAD/favicon.png -------------------------------------------------------------------------------- /search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/discovery-app-react/HEAD/search.png -------------------------------------------------------------------------------- /src/dispatchers/Dispatcher.js: -------------------------------------------------------------------------------- 1 | import {Dispatcher} from 'flux' 2 | export default new Dispatcher() 3 | -------------------------------------------------------------------------------- /src/utils/format-date.js: -------------------------------------------------------------------------------- 1 | export default function formatDate (date) { 2 | return (new Date(date)).toUTCString() 3 | } 4 | -------------------------------------------------------------------------------- /contentful_logo_120x90@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/discovery-app-react/HEAD/contentful_logo_120x90@2x.png -------------------------------------------------------------------------------- /src/utils/getID.js: -------------------------------------------------------------------------------- 1 | let lastId = 0 2 | export default function (prefix = 'id') { 3 | lastId++ 4 | return `${prefix}${lastId}` 5 | } 6 | -------------------------------------------------------------------------------- /src/components/assets/AssetsContainer.css: -------------------------------------------------------------------------------- 1 | .assets{ 2 | background: white; 3 | height: 90%; 4 | height: 90vh; 5 | padding: 20px; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/is-preview-set-in-query.js: -------------------------------------------------------------------------------- 1 | export default function isPreviewSetInQuery (query) { 2 | return 'preview' in query && query.preview !== 'false' && query.preview !== '' 3 | } 4 | -------------------------------------------------------------------------------- /src/components/requests/Request.css: -------------------------------------------------------------------------------- 1 | .request-meta { 2 | margin: 25px 25px 25px 0; 3 | background: #fff; 4 | border: 1px solid #e5e5e5; 5 | border-radius: 3px; 6 | padding: 25px; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/assets/Thumbnail.css: -------------------------------------------------------------------------------- 1 | .file-icon { 2 | border: 1px dashed #5B9FEF; 3 | text-overflow: ellipsis; 4 | overflow: hidden; 5 | padding: 5px; 6 | display: flex; 7 | align-items: center; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/long-name-short.js: -------------------------------------------------------------------------------- 1 | export function longNameShort (name, maxLength) { 2 | if (name.length > maxLength) { 3 | name = name.substring(0, (maxLength - 3)) 4 | name += '...' 5 | } 6 | return name 7 | } 8 | -------------------------------------------------------------------------------- /src/actions/actions.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../dispatchers/Dispatcher' 2 | 3 | export function changeTokenType (isPreview) { 4 | Dispatcher.dispatch({ 5 | type: 'API_SELECTION_CHANGE', 6 | isPreview 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6 2 | 3 | RUN mkdir -p /usr/src/app 4 | VOLUME "./:/usr/src/app" 5 | WORKDIR /usr/src/app 6 | RUN npm set progress=false && \ 7 | npm install -g --progress=false yarn 8 | COPY package.json ./ 9 | COPY yarn.lock ./ 10 | RUN yarn 11 | COPY ./ ./ 12 | EXPOSE 9020 13 | 14 | CMD ["yarn", "start"] 15 | -------------------------------------------------------------------------------- /src/components/NoMatch.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router' 3 | 4 | export default function NoMatch ({location}) { 5 | return

Oops! You're looking for something that isn't here. You probably want to go back to the main page.

6 | } 7 | -------------------------------------------------------------------------------- /src/components/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Error ({location}) { 4 | const error = (location.state.message) 5 | ?

An error occurred:

{location.state.message}

6 | :

An unexpected error occurred. Try resetting your credentials

7 | 8 | return
{error}
9 | } 10 | -------------------------------------------------------------------------------- /src/reducers/requests.js: -------------------------------------------------------------------------------- 1 | export function requests (state = [], action) { 2 | switch (action.type) { 3 | case 'APPEND_REQUEST_FULFILLED': 4 | // last request should appear first 5 | return [action.payload].concat(state.slice()) 6 | case 'RESET_REQUESTS': 7 | return state.filter(() => { return false }) 8 | } 9 | return state 10 | } 11 | -------------------------------------------------------------------------------- /src/components/NotificationLink.css: -------------------------------------------------------------------------------- 1 | .counter { 2 | color: white; 3 | display: inline-block; 4 | position: relative; 5 | padding: 2px 5px; 6 | } 7 | .counter .badge{ 8 | background-color: #fa3e3e; 9 | border-radius: 2px; 10 | color: white; 11 | 12 | padding: 1px 3px; 13 | font-size: 10px; 14 | 15 | position: absolute; 16 | top: -10px; 17 | right: 5px; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/tabs/Pane.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | const Pane = React.createClass({ 3 | displayName: 'Pane', 4 | propTypes: { 5 | label: React.PropTypes.string.isRequired, 6 | children: React.PropTypes.element.isRequired 7 | }, 8 | render () { 9 | return ( 10 |
11 | {this.props.children} 12 |
13 | ) 14 | } 15 | }) 16 | 17 | export default Pane 18 | -------------------------------------------------------------------------------- /src/components/assets/Search.css: -------------------------------------------------------------------------------- 1 | .search-container{ 2 | display: flex; 3 | justify-content: space-between; 4 | margin-top: 30px; 5 | padding: 20px; 6 | color: #AABAC1; 7 | } 8 | .search{ 9 | border:1px solid #d0d0d0; 10 | width: 50%; 11 | height: 40px; 12 | font-size: 1.2em; 13 | margin-left: 35px; 14 | padding: 10px 10px 10px 20px; 15 | background: url('../../../search.png') no-repeat 8px 10px; 16 | } 17 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { routerReducer } from 'react-router-redux' 3 | import { contentTypes } from './contentTypes' 4 | import { entries } from './entries' 5 | import { requests } from './requests' 6 | import { api } from './api' 7 | 8 | const rootReducer = combineReducers({api, contentTypes, entries, requests, routing: routerReducer}) 9 | 10 | export default rootReducer 11 | -------------------------------------------------------------------------------- /main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | border: 0; 6 | } 7 | 8 | body { 9 | padding: 0; 10 | margin: 0; 11 | border: none; 12 | font-family: 'Avenir', sans-serif; 13 | background-color: #F7F9FA; 14 | } 15 | 16 | main { 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | left: 0; 21 | right: 0; 22 | } 23 | 24 | a { 25 | border-bottom: 1px dotted #5B9FEF; 26 | color: #4A90E2; 27 | text-decoration: none; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/assets/Assets.css: -------------------------------------------------------------------------------- 1 | .asset-list { 2 | display: flex; 3 | flex-wrap: wrap; 4 | margin-top: 50px; 5 | padding: 20px; 6 | } 7 | 8 | .asset-item { 9 | width: 150px; 10 | height: 150px; 11 | margin-right: 15px; 12 | margin-bottom: 35px; 13 | text-align: center; 14 | border-bottom: none; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: flex-start; 18 | align-items: center; 19 | } 20 | 21 | .asset-label { 22 | margin-top: 15px; 23 | } 24 | -------------------------------------------------------------------------------- /src/reducers/contentTypes.js: -------------------------------------------------------------------------------- 1 | export function contentTypes (state = {}, action) { 2 | switch (action.type) { 3 | case 'FETCH_CONTENT_TYPES_PENDING': 4 | return Object.assign({}, state, {fetching: true}) 5 | case 'FETCH_CONTENT_TYPES_FULFILLED': 6 | return Object.assign({}, state, {fetching: false, payload: action.payload}) 7 | case 'FETCH_CONTENT_TYPES_REJECTED': 8 | return Object.assign({}, state, {fetching: false, payload: null, validationError: action.payload}) 9 | } 10 | return state 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.2" 4 | cache: yarn 5 | script: 6 | - npm run lint 7 | - npm run build 8 | - mkdir discovery_app 9 | - cp *.svg *.png *.css dist/*.js discovery_app 10 | - cp index.html discovery_app/index.html 11 | deploy: 12 | provider: s3 13 | access_key_id : $AWS_ACCESS_KEY_ID 14 | secret_access_key: $AWS_SECRET_ACCESS_KEY 15 | bucket: "discovery.contentful.com" 16 | acl: public_read 17 | local_dir: discovery_app 18 | skip_cleanup: true 19 | on: 20 | branch: master 21 | -------------------------------------------------------------------------------- /bin/analytics.json: -------------------------------------------------------------------------------- 1 | { 2 | "ANALYTICS": " " 3 | } 4 | -------------------------------------------------------------------------------- /src/components/assets/Search.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import styles from './Search.css' 4 | 5 | function Search ({itemCount, label, onChange}) { 6 | function _onChangeHandler (e) { 7 | onChange(e.target.value) 8 | } 9 | return
10 | 11 | {itemCount} {label} 12 |
13 | } 14 | 15 | export default CSSModules(Search, styles) 16 | -------------------------------------------------------------------------------- /src/components/tabs/Tabs.css: -------------------------------------------------------------------------------- 1 | .tabs { 2 | margin: 25px 25px 25px 0; 3 | background: #fff; 4 | border: 1px solid #e5e5e5; 5 | border-radius: 3px; 6 | } 7 | .tabs__labels { 8 | margin: 0; 9 | padding: 0; 10 | } 11 | .tabs__labels li { 12 | display: inline-block; 13 | } 14 | .tabs__labels li a { 15 | padding: 8px 12px; 16 | display: block; 17 | color: #444; 18 | text-decoration: none; 19 | border-bottom: 2px solid #f5f5f5; 20 | } 21 | .tabs__labels li a.active { 22 | border-bottom-color: #337ab7; 23 | } 24 | .tabs__content { 25 | padding: 25px; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/content-types/ContentTypeListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import {current} from '../List.css' 4 | export default function ContentTypeListItem ({item, location}) { 5 | function getClassIfCurrentlySelected () { 6 | return location.pathname.split('/').indexOf(item.sys.id) >= 0 ? current : '' 7 | } 8 | return ( 9 |
  • 10 | {item.name} 11 |
  • 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/TwoPanelList.css: -------------------------------------------------------------------------------- 1 | .two-panel-list { 2 | display: flex; 3 | border-left: 1px solid #CAD5DA; 4 | border-bottom : 1px solid #CAD5DA; 5 | } 6 | .list-item-contents { 7 | padding:40px 5px 0 35px; 8 | width: 750px; 9 | min-height: 300px; 10 | position: relative; 11 | border-left: 1px solid #CAD5DA; 12 | background-color: white; 13 | } 14 | 15 | .placeholder { 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | align-items: center; 20 | position: absolute; 21 | top: 0; 22 | bottom: 0; 23 | left: 0; 24 | right: 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Main.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux' 2 | import { connect } from 'react-redux' 3 | import * as actionCreators from '../actions/actionCreators' 4 | import App from './App' 5 | 6 | function mapStateToProps (state) { 7 | return { 8 | api: state.api, 9 | contentTypes: state.contentTypes, 10 | entries: state.entries, 11 | requests: state.requests 12 | } 13 | } 14 | 15 | function mapDispatchToProps (dispatch) { 16 | return bindActionCreators(actionCreators, dispatch) 17 | } 18 | 19 | const Main = connect(mapStateToProps, mapDispatchToProps)(App) 20 | 21 | export default Main 22 | -------------------------------------------------------------------------------- /src/components/requests/RequestListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import {current} from '../List.css' 4 | 5 | export default function RequestListItem ({item, i, location}) { 6 | function getClassIfCurrentlySelected () { 7 | const pathnames = location.pathname.split('/') 8 | if (pathnames.indexOf(`${i}`) >= 0) { 9 | return current 10 | } 11 | return '' 12 | } 13 | 14 | return ( 15 |
  • 16 | {`[${item.time}] ${item.path}`} 17 |
  • 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/reducers/api.js: -------------------------------------------------------------------------------- 1 | export function api (state = {}, action) { 2 | switch (action.type) { 3 | case 'TOGGLE_API_PREVIEW': 4 | return Object.assign({}, state, {selectedApi: action.isPreview ? 'preview' : 'delivery'}) 5 | case '@@router/LOCATION_CHANGE': 6 | const {query} = action.payload 7 | return Object.assign({}, state, { 8 | deliveryAccessToken: query.delivery_access_token || '', 9 | space: query.space_id || '', 10 | previewAccessToken: query.preview_access_token || '', 11 | selectedApi: (query.preview && query.preview === 'true') ? 'preview' : 'delivery' 12 | }) 13 | 14 | } 15 | return state 16 | } 17 | -------------------------------------------------------------------------------- /src/reducers/entries.js: -------------------------------------------------------------------------------- 1 | import scour from 'scourjs' 2 | 3 | export function entries (state = {}, action) { 4 | switch (action.type) { 5 | case 'FETCH_ENTRIES_PENDING': 6 | return Object.assign({}, state, {fetching: true}) 7 | case 'FETCH_ENTRIES_FULFILLED': 8 | const {payload} = action 9 | return Object.assign({}, state, {fetching: false, 10 | entry: payload.entry, 11 | payload: payload.entries, 12 | skip: payload.skip, 13 | total: payload.total}) 14 | case 'FETCH_ENTRIES_REJECTED': 15 | return Object.assign({}, state, {fetching: false, error: action.payload, payload: scour([])}) 16 | } 17 | return state 18 | } 19 | -------------------------------------------------------------------------------- /src/components/List.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import styles from './List.css' 4 | 5 | class List extends React.Component { 6 | componentWillMout () { 7 | } 8 | render () { 9 | return ( 10 |
    11 | {this.props.TitleView} 12 |
    13 | 14 | {this.props.ListActionView} 15 |
    16 |
    17 | ) 18 | } 19 | } 20 | 21 | List.propTypes = { 22 | TitleView: PropTypes.object.isRequired, 23 | list: PropTypes.array.isRequired 24 | } 25 | 26 | export default CSSModules(List, styles) 27 | -------------------------------------------------------------------------------- /src/components/assets/Assets.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Link} from 'react-router' 3 | import CSSModules from 'react-css-modules' 4 | import styles from './Assets.css' 5 | import Thumbnail from './Thumbnail' 6 | 7 | function Assets ({items, location}) { 8 | const assets = items.map((item) => { 9 | return 10 | 11 |

    {item.fields.title}

    12 | 13 | }) 14 | return
    {assets}
    15 | } 16 | 17 | export default CSSModules(Assets, styles) 18 | -------------------------------------------------------------------------------- /src/components/assets/AssetContainer.js: -------------------------------------------------------------------------------- 1 | import React, {createClass} from 'react' 2 | import {getClient} from '../../services/contentfulClient' 3 | import Asset from './Asset' 4 | 5 | export default createClass({ 6 | getInitialState () { 7 | return { 8 | phase: 'loading' 9 | } 10 | }, 11 | 12 | componentDidMount () { 13 | getClient().getAsset(this.props.params.assetId) 14 | .then((asset) => { 15 | this.setState({ 16 | asset: asset.toPlainObject(), 17 | phase: 'loaded' 18 | }) 19 | }) 20 | }, 21 | 22 | render () { 23 | if (this.state.phase === 'loading') { 24 | return

    Loading your Asset...

    25 | } else { 26 | return 27 | } 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/assets/Thumbnail.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import styles from './Thumbnail.css' 4 | 5 | function Thumbnail ({url, fileName, description, width = 70, height = 70}) { 6 | if (/^\/\/images\./.test(url)) { 7 | return {fileName} 13 | } else if (fileName) { 14 | const style = { 15 | width: width + 'px', 16 | height: height + 'px' 17 | } 18 | return
    19 | {fileName} 20 |
    21 | } 22 | return '' 23 | } 24 | 25 | export default CSSModules(Thumbnail, styles) 26 | -------------------------------------------------------------------------------- /src/components/NotificationLink.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import styles from './NotificationLink.css' 4 | import { Link } from 'react-router' 5 | 6 | class NotificationLink extends React.Component { 7 | render () { 8 | return ( 9 |
    10 |
    11 | {this.props.count} 12 |
    13 | 14 | {this.props.label} 15 | 16 |
    17 | ) 18 | } 19 | } 20 | 21 | NotificationLink.propTypes = { 22 | label: PropTypes.string.isRequired, 23 | count: PropTypes.number.isRequired, 24 | to: PropTypes.object.isRequired 25 | } 26 | 27 | export default CSSModules(NotificationLink, styles) 28 | -------------------------------------------------------------------------------- /src/components/entries/EntryListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import { current } from '../List.css' 4 | 5 | export default function EntryListItem ({item, location}) { 6 | const contentType = item.sys.contentType 7 | function getClassIfCurrentlySelected () { 8 | const pathnames = location.pathname.split('/') 9 | if (pathnames.indexOf(contentType.sys.id) >= 0 && pathnames.indexOf(item.sys.id) >= 0) { 10 | return current 11 | } 12 | return '' 13 | } 14 | return ( 15 |
  • 16 | 17 | {item.fields[contentType.displayField] || 'Untitled'} 18 | 19 |
  • 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/List.css: -------------------------------------------------------------------------------- 1 | .list { 2 | width: 250px; 3 | height: 100%; 4 | height:100vh; 5 | border-left: 1px solid #CAD5DA; 6 | } 7 | 8 | .list-container { 9 | margin-top: 10px; 10 | height: 94%; 11 | overflow: auto; 12 | padding-right: 5px; 13 | } 14 | 15 | .list ul { 16 | padding: 0; 17 | margin: 0; 18 | } 19 | .list h3 { 20 | background-color: #DEE4E9; 21 | padding: 15px; 22 | color: #2A3038; 23 | border-bottom: 1px solid #CAD5DA; 24 | } 25 | 26 | .list li { 27 | padding: 7px 5px; 28 | margin: 0; 29 | list-style: none; 30 | } 31 | .current{ 32 | background-color: #DEE4E9; 33 | cursor:default; 34 | } 35 | .list li:hover { 36 | background-color: #DEE4E9; 37 | cursor: pointer; 38 | } 39 | 40 | .list a { 41 | border-bottom: none; 42 | color: #2C333B; 43 | font-size: 15px; 44 | display: block; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/content-types/ContentTypesContainer.js: -------------------------------------------------------------------------------- 1 | import React, { createClass } from 'react' 2 | import TwoPanelList, { Placeholder } from '../TwoPanelList' 3 | import ContentTypeListItem from './ContentTypeListItem' 4 | 5 | export default createClass({ 6 | componentDidMount () { 7 | this.props.getContentTypes() 8 | }, 9 | render () { 10 | if (this.props.contentTypes.fetching === true) { 11 | return

    Loading your Content Types...

    12 | } else { 13 | const listTitle =

    Content Types

    14 | const placeholder = 15 | 16 | return 19 | } 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /bin/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | PAGES_DIR=./gh-pages 5 | REPO="git@github.com:contentful/contentful-react-discovery.git" 6 | 7 | echo "Publishing" 8 | 9 | # get the gh-pages branch of the repo 10 | if [ ! -d $PAGES_DIR ] ; then 11 | git clone --single-branch --branch gh-pages $REPO $PAGES_DIR 12 | fi 13 | 14 | cp *.svg *.png *.css dist/*.js $PAGES_DIR 15 | 16 | # Setup base path and analytics tag 17 | cat index.html | \ 18 | sed -e 's//{{{ANALYTICS}}}/g' > \ 19 | $PAGES_DIR/index.mustache 20 | 21 | ./node_modules/.bin/mustache ./bin/analytics.json $PAGES_DIR/index.mustache > $PAGES_DIR/index.html 22 | rm -f $PAGES_DIR/index.mustache 23 | 24 | cp $PAGES_DIR/index.html $PAGES_DIR/404.html 25 | 26 | pushd $PAGES_DIR 27 | git add . 28 | git commit -a -m "Docs update" 29 | if [ $? -eq 1 ] ; then 30 | echo "Nothing to update" 31 | else 32 | git push origin gh-pages 33 | fi 34 | popd 35 | -------------------------------------------------------------------------------- /src/components/assets/Asset.css: -------------------------------------------------------------------------------- 1 | .asset-container { 2 | display: flex; 3 | justify-content: flex-start; 4 | flex-direction: column; 5 | } 6 | 7 | .preview { 8 | margin: 50px auto; 9 | } 10 | 11 | .preview a { 12 | border-bottom: none; 13 | } 14 | 15 | .metadata { 16 | width: 100%; 17 | background-color: white; 18 | border-top: 1px solid #DBE3E7; 19 | border-bottom: 1px solid #DBE3E7; 20 | display: flex; 21 | padding: 40px 20px; 22 | justify-content: flex-start; 23 | composes: field from '../entries/Field.css' 24 | } 25 | 26 | .metadata > section { 27 | overflow-wrap: break-word; 28 | width: 50%; 29 | padding:20px; 30 | } 31 | 32 | .metadata > section > div { 33 | margin-top: 25px; 34 | } 35 | .metadata h3{ 36 | color: #95A3B3; 37 | font-weight: lighter; 38 | text-transform:uppercase; 39 | } 40 | .left{ 41 | border-right: 1px solid #CAD5DA; 42 | } 43 | .metadata h1 { 44 | margin-bottom: 20px; 45 | } 46 | -------------------------------------------------------------------------------- /src/services/contentfulClient.js: -------------------------------------------------------------------------------- 1 | import { createClient } from 'contentful' 2 | 3 | let client 4 | let authorized 5 | let currentSpace 6 | 7 | function initClient (space, accessToken, preview) { 8 | client = createClient({ 9 | space: space, 10 | accessToken: accessToken, 11 | host: preview ? 'preview.contentful.com' : 'cdn.contentful.com' 12 | }) 13 | return client.getSpace() 14 | .then((space) => { 15 | authorized = true 16 | currentSpace = space 17 | return space 18 | }) 19 | } 20 | 21 | function getClient () { 22 | return authorized && client 23 | } 24 | 25 | function getCurrentSpaceName () { 26 | let currentSpaceName = (currentSpace && currentSpace.name) ? currentSpace.name : '' 27 | return currentSpaceName 28 | } 29 | 30 | function resetClient () { 31 | window.sessionStorage.clear() 32 | authorized = false 33 | } 34 | 35 | export { initClient, getClient, resetClient, getCurrentSpaceName } 36 | -------------------------------------------------------------------------------- /src/components/requests/Requests.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TwoPanelList, {Placeholder} from '../TwoPanelList' 3 | import RequestListItem from './RequestListItem' 4 | import Request from './Request' 5 | 6 | export default class Requests extends React.Component { 7 | 8 | render () { 9 | const listTitle =

    Requests History

    10 | let content = 11 | const {requestId} = this.props.params 12 | const {requests} = this.props 13 | const request = (requestId && requestId <= requests.length - 1) ? requests[requestId] : undefined 14 | 15 | if (request) { 16 | content = 17 | } 18 | return 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Discovery App 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/entries/Field.css: -------------------------------------------------------------------------------- 1 | .field { 2 | margin-bottom: 30px; 3 | } 4 | 5 | 6 | .field ul { 7 | padding-left: 25px; 8 | } 9 | 10 | .field > h2, 11 | .field p, 12 | .field blockquote, 13 | .field ul { 14 | margin-bottom: 10px; 15 | } 16 | 17 | .image-link { 18 | border-bottom: none; 19 | } 20 | 21 | .entries-list {} 22 | 23 | .entries-item {} 24 | 25 | .asset-list { 26 | display: flex; 27 | } 28 | 29 | .asset-item { 30 | width: 70px; 31 | height: 70px; 32 | margin-right: 10px; 33 | } 34 | 35 | .symbol-list { 36 | display: flex; 37 | } 38 | 39 | .symbol-list > div { 40 | } 41 | 42 | .symbol-item { 43 | margin-right: 5px; 44 | } 45 | 46 | .symbol-item > p:after { 47 | content: ',' 48 | } 49 | 50 | .symbol-item:last-child > p:after { 51 | content: '' 52 | } 53 | .edit-section{ 54 | width: 120%; 55 | height: 50px; 56 | padding: 10px; 57 | background-color: #F7F9FA; 58 | margin: -40px 0px 50px -35px; 59 | } 60 | .edit-link{ 61 | /*TODO: Add edit icon here*/ 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { Link } from 'react-router' 3 | import { getCurrentSpaceName } from '../services/contentfulClient' 4 | import { longNameShort } from '../utils/long-name-short.js' 5 | 6 | export default class Nav extends React.Component { 7 | render () { 8 | const q = this.props.location.query 9 | return ( 10 |
      11 |
    • 12 | {longNameShort(getCurrentSpaceName(), 13)} [ 13 | Change 14 | ] 15 |
    • 16 |
    • 17 | Entries 18 | 19 |
    • 20 |
    • 21 | Media Library 22 | 23 |
    • 24 |
    25 | ) 26 | } 27 | } 28 | 29 | Nav.propTypes = { 30 | location: PropTypes.object.isRequired 31 | } 32 | 33 | Nav.contextTypes = { 34 | router: PropTypes.object.isRequired 35 | } 36 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | module.exports = { 4 | context: path.join(__dirname, 'src'), 5 | entry: './main.js', 6 | output: { 7 | path: path.join(__dirname, 'dist'), 8 | filename: '[name].bundle.js', 9 | publicPath: '/' 10 | }, 11 | devtool: 'source-map', 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js?$/, 16 | exclude: /(node_modules|bower_components|dist)/, 17 | loader: 'babel' 18 | }, 19 | { 20 | test: /\.css$/, 21 | loaders: [ 22 | 'style?sourceMap', 23 | 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]' 24 | ] 25 | }, 26 | { test: /\.jpe?g$|\.svg$|\.png$/, 27 | exclude: /node_modules/, 28 | loader: 'file-loader?name=[path][name].[ext]' 29 | } 30 | ] 31 | }, 32 | plugins: [ 33 | new webpack.EnvironmentPlugin([ 34 | 'NODE_ENV' 35 | ]) 36 | ], 37 | devServer: { 38 | historyApiFallback: true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/entries/EntryLinkContainer.js: -------------------------------------------------------------------------------- 1 | import React, {createClass, PropTypes} from 'react' 2 | import {Link} from 'react-router' 3 | import {findContentType} from '../../services/contentTypeStore' 4 | 5 | export default createClass({ 6 | propTypes: { 7 | entryLink: PropTypes.object.isRequired 8 | }, 9 | 10 | getInitialState () { 11 | return { 12 | phase: 'loading' 13 | } 14 | }, 15 | 16 | componentDidMount () { 17 | findContentType(this.props.entryLink.sys.contentType.sys.id) 18 | .then((contentType) => { 19 | this.setState({ 20 | contentType: contentType, 21 | phase: 'loaded' 22 | }) 23 | }) 24 | }, 25 | 26 | render () { 27 | if (this.state.phase === 'loading') { 28 | return

    Loading Link...

    29 | } else { 30 | const displayField = this.props.entryLink.fields[this.state.contentType.displayField] 31 | const entryLinkSys = this.props.entryLink.sys 32 | return 33 | {displayField || "untitled"} 34 | 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/services/contentTypeStore.js: -------------------------------------------------------------------------------- 1 | import {getClient} from './contentfulClient' 2 | 3 | let contentTypesStorage 4 | 5 | function loadFromStorage () { 6 | const stringifiedContentTypes = window.sessionStorage.getItem('contentTypes') 7 | contentTypesStorage = stringifiedContentTypes ? JSON.parse(stringifiedContentTypes) : null 8 | } 9 | 10 | function getContentTypes () { 11 | loadFromStorage() 12 | if (contentTypesStorage) { 13 | return Promise.resolve(contentTypesStorage) 14 | } else { 15 | return getClient().getContentTypes() 16 | .then((response) => { 17 | storeContentTypes(response.items) 18 | return contentTypesStorage 19 | }) 20 | } 21 | } 22 | 23 | function findContentTypeInList (contentTypes, id) { 24 | return contentTypes.find((item) => item.sys.id === id) 25 | } 26 | 27 | function findContentType (id) { 28 | return getContentTypes() 29 | .then((contentTypes) => findContentTypeInList(contentTypes, id)) 30 | } 31 | 32 | function storeContentTypes (contentTypes) { 33 | contentTypesStorage = contentTypes 34 | window.sessionStorage.setItem('contentTypes', JSON.stringify(contentTypes)) 35 | } 36 | 37 | loadFromStorage() 38 | 39 | export { 40 | storeContentTypes, 41 | getContentTypes, 42 | findContentTypeInList, 43 | findContentType 44 | } 45 | -------------------------------------------------------------------------------- /src/components/TwoPanelList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import styles from './TwoPanelList.css' 4 | import List from './List' 5 | 6 | function TwoPanelList ({items, ContentView, location}) { 7 | const lists = items.map((topItem, index) => { 8 | const list = topItem.items.map((innerItem, innerIndex) => { 9 | return 14 | }) 15 | return 20 | }) 21 | return ( 22 |
    23 | {lists} 24 |
    25 | {ContentView} 26 |
    27 |
    28 | ) 29 | } 30 | 31 | TwoPanelList.propTypes = { 32 | items: PropTypes.array.isRequired, 33 | ContentView: PropTypes.element, 34 | location: PropTypes.object.isRequired 35 | } 36 | 37 | export default CSSModules(TwoPanelList, styles) 38 | 39 | export const Placeholder = CSSModules(({content}) => { 40 | return
    41 | 42 |

    43 | {content} 44 |

    45 |
    46 | }, styles) 47 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import logger from 'redux-logger' 3 | import thunk from 'redux-thunk' 4 | import promiseMiddleware from 'redux-promise-middleware' 5 | import { syncHistoryWithStore } from 'react-router-redux' 6 | import { browserHistory } from 'react-router' 7 | import scour from 'scourjs' 8 | import rootReducer from './reducers/index' 9 | 10 | const initialState = { 11 | api: { 12 | deliveryAccessToken: '', 13 | previewAccessToken: '', 14 | selectedApi: 'delivery', 15 | space: '' 16 | }, 17 | contentTypes: { 18 | fetching: false, 19 | validationError: null, 20 | payload: [] 21 | }, 22 | entries: { 23 | fetching: false, 24 | entry: undefined, 25 | payload: scour([]), 26 | skip: 0, 27 | total: undefined, 28 | error: null 29 | }, 30 | requests: [] 31 | } 32 | const middleware = applyMiddleware(promiseMiddleware(), thunk, logger()) 33 | 34 | function RunDevToolExtensionIfNotInProduction () { 35 | const shouldExposeState = (!process.env.NODE_ENV || 36 | process.env.NODE_ENV !== 'production') && 37 | window.devToolsExtension 38 | return (shouldExposeState ? window.devToolsExtension() : (f) => f) 39 | } 40 | export const store = createStore(rootReducer, initialState, compose(middleware, 41 | RunDevToolExtensionIfNotInProduction() 42 | )) 43 | 44 | export const history = syncHistoryWithStore(browserHistory, store) 45 | -------------------------------------------------------------------------------- /src/components/requests/Request.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import JSONTree from 'react-json-tree' 3 | import Tabs from '../tabs/Tabs' 4 | import Pane from '../tabs/Pane' 5 | import styles from './Request.css' 6 | import CSSModules from 'react-css-modules' 7 | 8 | function Request ({request, location}) { 9 | function parseUrl (urlWithParams) { 10 | const urlSections = urlWithParams.split('?') 11 | const url = urlSections[0] 12 | const params = urlSections[1].split('&').map((item) => { 13 | const param = item.split('=') 14 | let paramObj = {} 15 | paramObj[`${param[0]}`] = param[1] 16 | return paramObj 17 | }) 18 | return { 19 | url, 20 | params 21 | } 22 | } 23 | const urlData = parseUrl(request.url) 24 | return ( 25 |
    26 |
    27 |

    Request URL: {urlData.url}

    28 |

    Request Method: GET

    29 |

    Request Paramaters:

    30 | 31 |
    32 | 33 | 34 |
    35 |
    36 | 37 |
    38 |
    39 |
    40 | 41 |
    42 | ) 43 | } 44 | 45 | export default CSSModules(Request, styles) 46 | -------------------------------------------------------------------------------- /src/components/App.css: -------------------------------------------------------------------------------- 1 | .centered-container { 2 | width: 1000px; 3 | margin: 0 auto; 4 | } 5 | 6 | .app-container { 7 | position: absolute; 8 | top: 0; 9 | bottom: 0; 10 | left: 0; 11 | right: 0; 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: flex-start; 15 | } 16 | 17 | .app-container nav { 18 | background: #192532; 19 | font-size: 0.92em; 20 | min-height: 60px; 21 | } 22 | .app-container nav ul { 23 | color: rgba(255,255,255,0.75); 24 | } 25 | .app-container nav ul li.selected{ 26 | color: white; 27 | } 28 | .nav-container { 29 | composes: centered-container; 30 | display: flex; 31 | justify-content: space-between; 32 | align-items: center; 33 | padding: 10px 0px; 34 | } 35 | 36 | .app-container nav ul { 37 | display: flex; 38 | justify-content: center; 39 | padding-top: 2px; 40 | } 41 | 42 | .app-container nav li { 43 | list-style: none; 44 | margin-left: 20px; 45 | } 46 | 47 | .app-container nav a { 48 | display: inline-block; 49 | color: rgba(255,255,255,0.75); 50 | border-bottom: none; 51 | } 52 | 53 | .app-container nav a:hover { 54 | color: white; 55 | } 56 | 57 | .content-container { 58 | composes: centered-container; 59 | } 60 | .logo span{ 61 | display: block; 62 | height: 32px; 63 | float: right; 64 | margin-top: 5px; 65 | color: white; 66 | } 67 | .app-container .requests-link{ 68 | margin-left: -130px; 69 | } 70 | .hidden { 71 | display: none; 72 | } 73 | .displayed { 74 | display: flex; 75 | } 76 | -------------------------------------------------------------------------------- /src/components/entries/Entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import scour from 'scourjs' 3 | import CSSModules from 'react-css-modules' 4 | import styles from './Field.css' 5 | import Field from './Field' 6 | 7 | function Entry ({entry, location}) { 8 | const contentType = scour(entry.sys.contentType) 9 | const displayFieldId = contentType.get('displayField') 10 | const fieldsData = contentType.go('fields') 11 | const fieldsWithoutDisplay = fieldsData.filter((field) => { 12 | return field.get('id') !== displayFieldId 13 | }) 14 | const fields = [] 15 | 16 | if (displayFieldId) { 17 | const displayField = fieldsData.find({id: contentType.get('displayField')}).value 18 | 19 | fields.push( 20 | 21 | ) 22 | } 23 | 24 | fieldsWithoutDisplay.forEach((field) => { 25 | const id = field.get('id') 26 | fields.push( 27 | 28 | ) 29 | }) 30 | 31 | return ( 32 |
    33 |
    34 | Edit in Contentful Web App 39 |
    40 | {fields} 41 |
    42 | ) 43 | } 44 | 45 | export default CSSModules(Entry, styles) 46 | -------------------------------------------------------------------------------- /src/components/ToggleButton.css: -------------------------------------------------------------------------------- 1 | .onoffswitch { 2 | position: relative; width: 43px; 3 | -webkit-user-select:none; -moz-user-select:none; -ms-user-select: none; 4 | } 5 | .onoffswitch-checkbox { 6 | display: none; 7 | } 8 | .onoffswitch-label { 9 | display: block; overflow: hidden; cursor: pointer; 10 | border: 2px solid #999999; border-radius: 10px; 11 | } 12 | .onoffswitch-inner { 13 | display: block; width: 200%; margin-left: -100%; 14 | transition: margin 0.3s ease-in 0s; 15 | } 16 | .onoffswitch-inner:before, .onoffswitch-inner:after { 17 | display: block; float: left; width: 50%; height: 17px; padding: 0; line-height: 17px; 18 | font-size: 14px; color: white; font-family: Trebuchet, Arial, sans-serif; font-weight: bold; 19 | box-sizing: border-box; 20 | } 21 | .onoffswitch-inner:before { 22 | content: ""; 23 | padding-left: 10px; 24 | background-color: #21304A; color: #5A9EEE; 25 | } 26 | .onoffswitch-inner:after { 27 | content: ""; 28 | padding-right: 10px; 29 | background-color: #21304A; color: #FFFFFF; 30 | text-align: right; 31 | } 32 | .onoffswitch-switch { 33 | display: block; width: 16px; margin: 2px; 34 | background: #5A9EEE; 35 | position: absolute; top: 0; bottom: 0; 36 | right: 22px; 37 | border-radius: 10px; 38 | transition: all 0.1s ease-in 0s; 39 | } 40 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { 41 | margin-left: 0; 42 | } 43 | .onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { 44 | right: 0px; 45 | background-color: #5A9EEE; 46 | } 47 | .selected { 48 | color: #5695E0; 49 | } 50 | .idle{ 51 | color: white; 52 | } 53 | -------------------------------------------------------------------------------- /src/components/tabs/Tabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import styles from './Tabs.css' 4 | const Tabs = React.createClass({ 5 | displayName: 'Tabs', 6 | propTypes: { 7 | selected: React.PropTypes.number, 8 | children: React.PropTypes.oneOfType([ 9 | React.PropTypes.array, 10 | React.PropTypes.element 11 | ]).isRequired 12 | }, 13 | getDefaultProps () { 14 | return { 15 | selected: 0 16 | } 17 | }, 18 | getInitialState () { 19 | return { 20 | selected: this.props.selected 21 | } 22 | }, 23 | handleClick (index, event) { 24 | event.preventDefault() 25 | this.setState({ 26 | selected: index 27 | }) 28 | }, 29 | _renderTitles () { 30 | function labels (child, index) { 31 | let activeClass = (this.state.selected === index ? 'active' : '') 32 | return ( 33 |
  • 34 | 37 | {child.props.label} 38 | 39 |
  • 40 | ) 41 | } 42 | return ( 43 |
      44 | {this.props.children.map(labels.bind(this))} 45 |
    46 | ) 47 | }, 48 | _renderContent () { 49 | return ( 50 |
    51 | {this.props.children[this.state.selected]} 52 |
    53 | ) 54 | }, 55 | render () { 56 | return ( 57 |
    58 | {this._renderTitles()} 59 | {this._renderContent()} 60 |
    61 | ) 62 | } 63 | }) 64 | export default CSSModules(Tabs, styles) 65 | -------------------------------------------------------------------------------- /src/components/assets/AssetsContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { getClient } from '../../services/contentfulClient' 3 | import Assets from './Assets' 4 | import Search from './Search' 5 | import styles from './AssetsContainer.css' 6 | import CSSModules from 'react-css-modules' 7 | import update from 'react-addons-update' 8 | 9 | class AssetsContainer extends React.Component { 10 | constructor () { 11 | super() 12 | this.onChangeHandler = this.onChangeHandler.bind(this) 13 | this.state = { 14 | assets: {}, 15 | phase: 'loading' 16 | } 17 | } 18 | 19 | componentDidMount () { 20 | getClient().getAssets() 21 | .then((assets) => { 22 | this.initialAssets = assets.toPlainObject() 23 | this.setState({ 24 | assets: assets.toPlainObject(), 25 | phase: 'loaded' 26 | }) 27 | }) 28 | } 29 | 30 | onChangeHandler (filter) { 31 | filter = filter.trim().toLowerCase() 32 | const newItems = this.initialAssets.items.filter((asset) => { 33 | return asset.fields.title.trim().toLowerCase().match(filter) 34 | }) 35 | const newState = update(this.state, { 36 | assets: { 37 | items: { 38 | $set: newItems 39 | } 40 | } 41 | }) 42 | this.setState(newState) 43 | } 44 | 45 | render () { 46 | if (this.state.phase === 'loading') { 47 | return
    48 |

    Loading your Assets...

    49 |
    50 | } else if (this.state.assets.length === 0) { 51 | return
    52 |

    No assets are available.

    53 |
    54 | } 55 | return
    56 | 57 | 58 |
    59 | } 60 | } 61 | 62 | export default CSSModules(AssetsContainer, styles) 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | 3 | This repository is deprecated and no further maintenance is planned.For a Reference example app, please see [https://github.com/contentful/the-example-app.nodejs](https://github.com/contentful/the-example-app.nodejs) 4 | 5 | # About 6 | 7 | [Contentful](https://www.contentful.com) provides a content infrastructure for digital teams to power content in websites, apps, and devices. Unlike a CMS, Contentful was built to integrate with the modern software stack. It offers a central hub for structured content, powerful management and delivery APIs, and a customizable web app that enable developers and content creators to ship digital products faster. 8 | 9 | The Contentful Discovery web app gives you a quick and easy way to preview your content on a web environment, and explore the contents of your Spaces. 10 | 11 | You can try out the app at https://discovery.contentful.com/ or you can check out the source code and suggest your own improvements. 12 | 13 | # Running discovery-app locally 14 | 15 | ## Prepare 16 | 17 | clone the app and `cd` to the directory 18 | 19 | ```shell 20 | git clone https://github.com/contentful/discovery-app-react 21 | ``` 22 | 23 | ## Install dependencies via npm 24 | 25 | ```shell 26 | npm install 27 | ``` 28 | 29 | ## Install dependencies via yarn 30 | 31 | ```shell 32 | yarn 33 | ``` 34 | 35 | ## Start the app 36 | 37 | ```shell 38 | npm start 39 | ``` 40 | 41 | Open `http://0.0.0.0:9020` in your browser to see the app. 42 | 43 | Yarn users alternatively can use `yarn start` to start the app. 44 | 45 | # Using Docker 46 | 47 | clone the app and `cd` to the directory 48 | 49 | ```shell 50 | git clone https://github.com/contentful/discovery-app-react 51 | ``` 52 | 53 | Build the docker image 54 | 55 | ```shell 56 | docker build -t discovery-app . 57 | ``` 58 | 59 | Run the docker image 60 | 61 | ```shell 62 | docker run -it --rm -p 9020:9020 discovery-app 63 | ``` 64 | 65 | Open `http://0.0.0.0:9020` in your browser to see the app 66 | -------------------------------------------------------------------------------- /src/components/assets/Asset.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import prettyBytes from 'pretty-bytes' 4 | import formatDate from '../../utils/format-date' 5 | import styles from './Asset.css' 6 | import Thumbnail from './Thumbnail' 7 | 8 | function Asset ({asset}) { 9 | const {width, height} = asset.fields.file.details.image 10 | const imageWidth = width > '640' ? '640' : width 11 | const imageHeight = height > '480' ? '480' : height 12 | return
    13 |
    14 | 15 | 20 | 21 |
    22 |
    23 |
    24 |
    25 |

    Title

    26 |

    {asset.fields.title}

    27 |
    28 |
    29 |

    Description

    30 |

    31 | {asset.fields.description || 'No description provided.'} 32 |

    33 |
    34 |
    35 |

    Creation Date

    36 |

    37 | {formatDate(asset.sys.createdAt)} 38 |

    39 |
    40 |
    41 |
    42 |
    43 |

    File Name

    44 |

    45 | {asset.fields.file.fileName} 46 |

    47 |
    48 |
    49 |

    URL

    50 |

    51 | https:{asset.fields.file.url} 52 |

    53 |
    54 |
    55 |

    MIME Type

    56 |

    57 | {asset.fields.file.contentType} 58 |

    59 |
    60 |
    61 |

    Size

    62 |

    63 | {prettyBytes(asset.fields.file.details.size)} 64 |

    65 |
    66 |
    67 |
    68 |
    69 | } 70 | 71 | export default CSSModules(Asset, styles) 72 | -------------------------------------------------------------------------------- /src/components/ToggleButton.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import styles from './ToggleButton.css' 3 | import CSSModules from 'react-css-modules' 4 | import getID from '../utils/getID' 5 | 6 | class ToggleButton extends React.Component { 7 | componentWillMount () { 8 | this.state = {isChecked: this.props.checked, disabled: !!this.props.disabled} 9 | this.onChange = this.onChange.bind(this) 10 | this.toggleId = getID() 11 | } 12 | isSelectedWhen (flag) { 13 | if (this.state.isChecked === flag) { 14 | return 'selected' 15 | } 16 | return 'idle' 17 | } 18 | componentWillReceiveProps (nextProps) { 19 | if (this.props.checked !== nextProps.checked || this.props.disabled !== nextProps.disabled) { 20 | this.setState({isChecked: nextProps.checked, disabled: nextProps.disabled}) 21 | } 22 | } 23 | onChange (e) { 24 | if (this.props.disabled) { 25 | e.preventDefault() 26 | return 27 | } 28 | let nextVal = !this.state.isChecked 29 | this.setState({ 30 | isChecked: nextVal 31 | }) 32 | this.props.changeHandler(nextVal) 33 | } 34 | render () { 35 | return ( 36 |
      37 |
    • 38 | {this.props.unCheckedLabel} 39 |
    • 40 |
    • 41 |
      42 | 49 | 53 |
      54 |
    • 55 |
    • 56 | {this.props.checkedLabel} 57 |
    • 58 |
    59 | ) 60 | } 61 | } 62 | 63 | ToggleButton.propTypes = { 64 | changeHandler: PropTypes.func, 65 | unCheckedLabel: PropTypes.string.isRequired, 66 | checkedLabel: PropTypes.string.isRequired, 67 | checked: PropTypes.bool.isRequired, 68 | disabled: PropTypes.bool 69 | } 70 | 71 | export default CSSModules(ToggleButton, styles) 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "discovery-app-react", 3 | "version": "1.0.0", 4 | "description": "A React.js app of the Contentful Discovery app", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=development webpack-dev-server --host=0.0.0.0 --port=9020 --inline --hot", 8 | "clean": "rimraf dist && rimraf gh-pages", 9 | "lint": "standard src/**/*.js", 10 | "build": "npm run clean && NODE_ENV=production BASE_PATH=discovery-app-react webpack -p", 11 | "publish": "npm run build && ./bin/publish.sh" 12 | }, 13 | "author": "contentful ", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "babel-core": "^6.0.0", 17 | "babel-eslint": "^7.1.1", 18 | "babel-loader": "^6.2.4", 19 | "babel-preset-es2015": "^6.6.0", 20 | "babel-preset-react": "^6.5.0", 21 | "css-loader": "^0.26.1", 22 | "file-loader": "^0.10.0", 23 | "mustache": "^2.2.1", 24 | "rimraf": "^2.5.4", 25 | "standard": "^8.6.0", 26 | "style-loader": "^0.13.0", 27 | "webpack": "^1.12.14", 28 | "webpack-dev-server": "^1.14.1" 29 | }, 30 | "dependencies": { 31 | "axios": "^0.12.0", 32 | "contentful": "^3.5.0", 33 | "flux": "^2.1.1", 34 | "history": "^2.0.1", 35 | "localforage": "^1.4.0", 36 | "marked": "^0.3.6", 37 | "pretty-bytes": "^3.0.1", 38 | "react": "^15.0.0-rc.1", 39 | "react-ace": "^3.4.1", 40 | "react-addons-update": "^15.1.0", 41 | "react-css-modules": "^3.7.5", 42 | "react-dom": "^15.0.0-rc.1", 43 | "react-json-tree": "^0.8.0", 44 | "react-redux": "^4.4.5", 45 | "react-router": "^2.0.0", 46 | "react-router-redux": "^4.0.5", 47 | "redux": "^3.5.2", 48 | "redux-logger": "^2.6.1", 49 | "redux-promise": "^0.5.3", 50 | "redux-promise-middleware": "^3.3.0", 51 | "redux-thunk": "^2.1.0", 52 | "scourjs": "^1.0.1", 53 | "unique-concat": "^0.2.2" 54 | }, 55 | "standard": { 56 | "parser": "babel-eslint" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/contentful/discovery-app-react.git" 61 | }, 62 | "keywords": [ 63 | "react", 64 | "contentful", 65 | "discovery" 66 | ], 67 | "bugs": { 68 | "url": "https://github.com/contentful/discovery-app-react/issues" 69 | }, 70 | "homepage": "https://github.com/contentful/discovery-app-react#readme" 71 | } 72 | -------------------------------------------------------------------------------- /src/services/entriesStore.js: -------------------------------------------------------------------------------- 1 | import scour from 'scourjs' 2 | import {getClient} from './contentfulClient' 3 | import {getContentTypes, findContentTypeInList} from './contentTypeStore' 4 | import concat from 'unique-concat' 5 | 6 | function loadEntries (entries, {entryId, contentTypeId, contentTypeChanged} = {}) { 7 | const skip = contentTypeChanged ? 0 : entries.skip 8 | return Promise.all([ 9 | getClient().getEntries({ 10 | content_type: contentTypeId, 11 | skip: skip || 0, 12 | limit: 100, 13 | order: 'sys.createdAt' 14 | }), 15 | getContentTypes(), 16 | findEntry(entryId, entries) 17 | ]).then(([entriesResponse, contentTypes, entry]) => { 18 | entriesResponse = scour(entriesResponse) 19 | .set('items', appendAndAugmentEntries( 20 | contentTypeChanged ? [] : entries.payload.value, 21 | entriesResponse.items, 22 | contentTypes 23 | )) 24 | return { 25 | entry: entry ? scour(addContentTypeToEntry(entry, contentTypes)) : entry, 26 | entries: entriesResponse.go('items'), 27 | skip: skip + entriesResponse.limit, 28 | contentTypes, 29 | total: entriesResponse.total 30 | } 31 | }) 32 | } 33 | 34 | function findEntry (id, entries) { 35 | if (!id) return Promise.resolve(undefined) 36 | const entry = entries.payload.find(({'sys.id': {'$eq': id}})) 37 | if (entry) { 38 | return Promise.resolve(entry.value) 39 | } else { 40 | return getClient().getEntries({'sys.id': id}) 41 | .then((response) => { 42 | if (response.total > 0) { 43 | return response.items[0] 44 | } else { 45 | throw new Error('Entry not found') 46 | } 47 | }) 48 | } 49 | } 50 | 51 | function appendAndAugmentEntries (existingEntries, newEntries, contentTypes) { 52 | return concat( 53 | existingEntries, 54 | addContentTypesToEntries(newEntries, contentTypes), 55 | (entry) => entry.sys.id 56 | ) 57 | } 58 | 59 | function addContentTypesToEntries (entries, contentTypes) { 60 | return entries.map((entry) => addContentTypeToEntry(entry, contentTypes)) 61 | } 62 | 63 | function addContentTypeToEntry (entry, contentTypes) { 64 | const contentTypeId = entry.sys.contentType.sys.id 65 | entry.sys.contentType = findContentTypeInList(contentTypes, contentTypeId) 66 | return entry 67 | } 68 | 69 | export { 70 | loadEntries 71 | } 72 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import styles from './App.css' 4 | import Nav from './Nav' 5 | import NotificationLink from './NotificationLink' 6 | import ToggleButton from './ToggleButton' 7 | import { getClient } from '../services/contentfulClient' 8 | 9 | class App extends React.Component { 10 | handleChange (isPreview) { 11 | const {api} = this.props 12 | const query = { 13 | preview_access_token: api.previewAccessToken, 14 | delivery_access_token: api.deliveryAccessToken, 15 | preview: isPreview, 16 | space_id: api.space 17 | } 18 | this.context.router.push({ 19 | pathname: '/entries/by-content-type', 20 | query: query 21 | }) 22 | } 23 | shouldDisplay () { 24 | if (!getClient()) { 25 | return 'hidden' 26 | } 27 | return 'displayed' 28 | } 29 | render () { 30 | return ( 31 |
    32 | 58 |
    59 | {React.cloneElement(this.props.children, this.props)} 60 |
    61 |
    62 | ) 63 | } 64 | } 65 | App.contextTypes = { 66 | router: PropTypes.object.isRequired 67 | } 68 | 69 | export default CSSModules(App, styles) 70 | -------------------------------------------------------------------------------- /src/components/settings/Settings.css: -------------------------------------------------------------------------------- 1 | .settings-form, 2 | .form-container { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: flex-start; 6 | margin-top: 40px; 7 | } 8 | .settings-form{ 9 | background-color: white; 10 | border: 1px solid #DBE3E7; 11 | padding: 20px; 12 | } 13 | .settings-container{ 14 | width: 720px; 15 | margin: 0 auto; 16 | } 17 | .settings-title{ 18 | text-align: center; 19 | font-weight: normal; 20 | text-transform: uppercase; 21 | color: #2A3038; 22 | margin:30px 0 30px 0; 23 | } 24 | .form-caption{ 25 | margin-top: 30px; 26 | } 27 | .form-caption h4{ 28 | color:#95A3B3; 29 | } 30 | .form-caption section{ 31 | padding: 10px; 32 | color: #BBC8CD; 33 | } 34 | .settings-form > p, 35 | .form-container > * { 36 | margin-bottom: 25px; 37 | } 38 | .form-container { 39 | margin: 35px 0 65px; 40 | } 41 | .settings-form div { 42 | text-align: left; 43 | } 44 | .hidden { 45 | height: 0; 46 | opacity: 0; 47 | transition: all 0.5s linear; 48 | } 49 | .shown { 50 | height: 370px; 51 | } 52 | .form-container > div > img { 53 | display: block; 54 | margin-top: 20px; 55 | } 56 | .form-container > div > div { 57 | padding-top: 15px; 58 | } 59 | .form-container > div > div > ol { 60 | padding: 0 20px 20px 20px; 61 | } 62 | .settings-form input, 63 | .settings-form select, 64 | .settings-form button { 65 | font-size: 14px; 66 | padding: 0 10px; 67 | height: 38px; 68 | border-radius: 2px; 69 | } 70 | .settings-form select{ 71 | display: none; 72 | } 73 | .settings-form input, 74 | .settings-form select { 75 | background-color: #F7F9FA; 76 | border: 1px solid #DBE3E7; 77 | color: #536171; 78 | } 79 | 80 | .settings-form input { 81 | width: 550px; 82 | padding: 10px; 83 | } 84 | .settings-form .spaceInput{ 85 | width: 350px; 86 | } 87 | .settings-form select { 88 | margin-right: 10px; 89 | } 90 | 91 | .settings-form .label-title { 92 | display: block; 93 | margin-bottom: 4px; 94 | font-size: 16px; 95 | font-weight: bold; 96 | } 97 | 98 | .settings-form button { 99 | width: 200px; 100 | border: 1px solid #2b67ad; 101 | background: linear-gradient(0deg, #3C80CF 0%, #5B9FEF 100%) no-repeat; 102 | color: white; 103 | } 104 | 105 | input.checkbox { 106 | width: auto; 107 | height: auto; 108 | line-height: 20px; 109 | vertical-align: middle; 110 | margin-right: 5px; 111 | } 112 | 113 | .error { 114 | color: red; 115 | } 116 | -------------------------------------------------------------------------------- /src/components/entries/EntriesContainer.js: -------------------------------------------------------------------------------- 1 | import React, { createClass } from 'react' 2 | import TwoPanelList, { Placeholder } from '../TwoPanelList' 3 | import EntryListItem from './EntryListItem' 4 | import Entry from './Entry' 5 | import ContentTypeListItem from '../content-types/ContentTypeListItem' 6 | 7 | let currentContentTypeID 8 | export default createClass({ 9 | componentDidMount () { 10 | const {contentTypeId} = this.props.params 11 | if (this.props.contentTypes.payload.length === 0) { 12 | this.props.getContentTypes() 13 | } 14 | this.props.loadEntries(this.props.entries, {entryId: this.props.params.entryId, 15 | contentTypeId: contentTypeId, 16 | contentTypeChanged: currentContentTypeID !== contentTypeId }) 17 | currentContentTypeID = contentTypeId 18 | }, 19 | 20 | componentWillReceiveProps (nextProps) { 21 | const {params: currentParams} = this.props 22 | const {params: nextParams} = nextProps 23 | if (currentParams.entryId !== nextParams.entryId || 24 | currentParams.contentTypeId !== nextParams.contentTypeId 25 | ) { 26 | this.props.loadEntries(this.props.entries, { 27 | entryId: nextParams.entryId, 28 | contentTypeId: nextParams.contentTypeId, 29 | contentTypeChanged: currentParams.contentTypeId !== nextParams.contentTypeId 30 | }) 31 | } 32 | }, 33 | loadEntries () { 34 | const {params} = this.props 35 | this.props.loadEntries(this.props.entries, {entryId: params.entryId, 36 | contentTypeId: params.contentTypeId, 37 | contentTypeChanged: currentContentTypeID === params.contentTypeId }) 38 | currentContentTypeID = params.contentTypeId 39 | }, 40 | render () { 41 | const {entries} = this.props 42 | if (entries.fetching === true) { 43 | return

    44 | Loading your Entries.... 45 |

    46 | } 47 | let contentElement 48 | const contentTypeListTitle =

    Content Types

    49 | const entriesListTitle =

    Entries

    50 | if (entries.entry) { 51 | contentElement = 52 | } else { 53 | contentElement = 54 | } 55 | 56 | return 57 | } 58 | }) 59 | -------------------------------------------------------------------------------- /src/actions/actionCreators.js: -------------------------------------------------------------------------------- 1 | import * as contentTypeServie from '../services/contentTypeStore' 2 | import * as entriesService from '../services/entriesStore' 3 | import { store } from '../store' 4 | import axios from 'axios' 5 | import { longNameShort } from '../utils/long-name-short.js' 6 | 7 | // since we are using promises already we can make redux-promise-middlware create 8 | // so actions automatically for us so if we pass in a promise in the payload ti will 9 | // dispatch ACTION_TYPE_PENDING, ACTION_TYPE_FULFILLED and ACTION_TYPE_REJECTED automatically 10 | export function getContentTypes () { 11 | return { 12 | type: 'FETCH_CONTENT_TYPES', 13 | payload: contentTypeServie.getContentTypes().then((payload) => { 14 | const path = '/content_types/?access_token=&skip=0&limit=100&order=sys.createdAt' 15 | const url = getRawRequestUrl(path) 16 | store.dispatch(appendRequest(url, path, payload)) 17 | return payload 18 | }) 19 | } 20 | } 21 | 22 | export function toggleAPIMode (isPreview) { 23 | return { 24 | type: 'TOGGLE_API_PREVIEW', 25 | isPreview 26 | } 27 | } 28 | 29 | export function loadEntries (entries, {entryId, contentTypeId, contentTypeChanged} = {}) { 30 | return { 31 | type: 'FETCH_ENTRIES', 32 | payload: entriesService.loadEntries(entries, {entryId, contentTypeId, contentTypeChanged}).then((payload) => { 33 | const path = `/entries/${entryId || ''}?content_type=${contentTypeId}&access_token=` 34 | const url = getRawRequestUrl(path) 35 | store.dispatch(appendRequest(url, path, payload)) 36 | return payload 37 | }) 38 | } 39 | } 40 | export function resetRequests () { 41 | return { 42 | type: 'RESET_REQUESTS' 43 | } 44 | } 45 | export function appendRequest (url, path, payload) { 46 | return { 47 | type: 'APPEND_REQUEST', 48 | payload: axios.get(url).then((response) => { 49 | return { 50 | parsedPayload: payload, 51 | rawPayload: response.data, 52 | path: longNameShort(path, 20), 53 | time: new Date().toLocaleTimeString(undefined, {hour12: false}).split(' ')[0], 54 | url 55 | } 56 | }) 57 | } 58 | } 59 | 60 | function getRawRequestUrl (path) { 61 | const {space, selectedApi} = store.getState().api 62 | const accessToken = selectedApi === 'preview' ? store.getState().api.previewAccessToken : store.getState().api.deliveryAccessToken 63 | let host = selectedApi === 'preview' ? '//preview.contentful.com' : '//cdn.contentful.com' 64 | let url = 'https:' + host + `/spaces/${space}${path}`.replace(//i, accessToken) 65 | return url 66 | } 67 | -------------------------------------------------------------------------------- /contentful.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/settings/SettingsContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import SettingsForm from './SettingsForm' 3 | import { resetClient } from '../../services/contentfulClient' 4 | import isPreviewSetInQuery from '../../utils/is-preview-set-in-query' 5 | 6 | export default class SettingsContainer extends React.Component { 7 | constructor (props) { 8 | super(props) 9 | let q = this.props.location.query 10 | this.state = { 11 | space: q.space_id || '', 12 | deliveryAccessToken: q.delivery_access_token || '', 13 | previewAccessToken: q.preview_access_token || '', 14 | selectedApi: isPreviewSetInQuery(q) ? 'preview' : 'delivery', 15 | validationError: null 16 | } 17 | } 18 | 19 | loadSpace (event) { 20 | event.preventDefault() 21 | if (!this.state.space) { 22 | return this.showError('You need to provide a Space ID') 23 | } else if (!this.state.previewAccessToken && !this.state.deliveryAccessToken) { 24 | return this.showError('You need to provide a at least one Access Token for Delivery or Preview API') 25 | } 26 | resetClient() 27 | this.props.resetRequests() 28 | const query = { 29 | preview_access_token: this.state.previewAccessToken, 30 | delivery_access_token: this.state.deliveryAccessToken, 31 | preview: this.previewSelected(), 32 | space_id: this.state.space 33 | } 34 | const {previewAccessToken, deliveryAccessToken} = this.state 35 | if (previewAccessToken && !deliveryAccessToken) { 36 | query.preview = true 37 | } else if (deliveryAccessToken && !previewAccessToken) { 38 | query.preview = false 39 | } 40 | 41 | this.context.router.push({ 42 | pathname: '/entries/by-content-type', 43 | query: query 44 | }) 45 | } 46 | 47 | handleChange (event) { 48 | switch (event.target.id) { 49 | case 'space': 50 | this.setState({ space: event.target.value }) 51 | break 52 | case 'deliveryAccessToken': 53 | this.setState({ deliveryAccessToken: event.target.value }) 54 | break 55 | case 'previewAccessToken': 56 | this.setState({ previewAccessToken: event.target.value }) 57 | break 58 | } 59 | } 60 | 61 | showError (message) { 62 | this.setState({ validationError: message }) 63 | } 64 | 65 | handleAccessTokenChange (accessToken) { 66 | if (this.previewSelected()) { 67 | this.setState({ 68 | previewAccessToken: accessToken, 69 | validationError: null 70 | }) 71 | } else { 72 | this.setState({ 73 | deliveryAccessToken: accessToken, 74 | validationError: null 75 | }) 76 | } 77 | } 78 | previewSelected () { 79 | return this.props.api.selectedApi === 'preview' 80 | } 81 | render () { 82 | return 91 | } 92 | } 93 | 94 | SettingsContainer.contextTypes = { 95 | router: PropTypes.object.isRequired 96 | } 97 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { Router, Route, IndexRoute, Redirect } from 'react-router' 4 | import { initClient, getClient, resetClient } from './services/contentfulClient' 5 | import Main from './components/Main' 6 | import SettingsContainer from './components/settings/SettingsContainer' 7 | import ContentTypesContainer from './components/content-types/ContentTypesContainer' 8 | import EntriesContainer from './components/entries/EntriesContainer' 9 | import Entry from './components/entries/Entry' 10 | import Request from './components/requests/Request' 11 | import AssetsContainer from './components/assets/AssetsContainer' 12 | import AssetContainer from './components/assets/AssetContainer' 13 | import Requests from './components/requests/Requests' 14 | import Error from './components/Error' 15 | import NoMatch from './components/NoMatch' 16 | import isPreviewSetInQuery from './utils/is-preview-set-in-query' 17 | import { Provider } from 'react-redux' 18 | import { store, history } from './store' 19 | 20 | let credentials = { 21 | accessToken: '', 22 | space: '' 23 | } 24 | 25 | const router = (( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | )) 46 | 47 | render(router, document.getElementsByTagName('main')[0]) 48 | 49 | /** 50 | * Checks if client has been initialized. 51 | * If not, initializes it, and in case of failure redirects to login page 52 | * If the client is already initialized, proceeds to the actual route 53 | */ 54 | function requireCredentials (nextState, replace, next) { 55 | const query = nextState.location.query 56 | const isPreview = isPreviewSetInQuery(query) 57 | const newCredentials = { 58 | accessToken: isPreview ? query.preview_access_token : query.delivery_access_token, 59 | previewAccessToken: query.preview_access_token, 60 | deliveryAccessToken: query.delivery_access_token, 61 | space: query.space_id, 62 | preview: isPreview 63 | } 64 | if (credentialsExist(newCredentials) && (!getClient() || credentialsAreDifferent(credentials, newCredentials))) { 65 | resetClient() 66 | initializeClient(newCredentials, next, replace) 67 | } else if (!query.space_id && !query.access_token) { 68 | replace('/') 69 | next() 70 | } else { 71 | next() 72 | } 73 | } 74 | 75 | function credentialsExist (credentials) { 76 | return credentials.accessToken && credentials.space 77 | } 78 | 79 | function credentialsAreDifferent (credentials, newCredentials) { 80 | return !( 81 | credentials.accessToken === newCredentials.accessToken && 82 | credentials.space === newCredentials.space 83 | ) 84 | } 85 | 86 | /** 87 | * Initializes the client and proceeds to the actual route. 88 | * In case of failure redirects to error page with message 89 | */ 90 | 91 | function initializeClient (newCredentials, next, replace) { 92 | initClient(newCredentials.space, newCredentials.accessToken, newCredentials.preview) 93 | .then( 94 | (space) => { 95 | console.log(space) 96 | credentials = newCredentials 97 | next() 98 | }, 99 | (err) => { 100 | replace({ 101 | pathname: '/error', 102 | state: { 103 | message: err.message 104 | } 105 | }) 106 | next() 107 | } 108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /src/components/entries/Field.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router' 3 | import CSSModules from 'react-css-modules' 4 | import styles from './Field.css' 5 | import formatDate from '../../utils/format-date' 6 | import Thumbnail from '../assets/Thumbnail' 7 | import EntryLinkContainer from './EntryLinkContainer' 8 | import marked from 'marked' 9 | 10 | function Field ({definition, content, location}) { 11 | return ( 12 |
    13 |

    {definition.name}

    14 | {renderContent(content, definition, location)} 15 |
    16 | ) 17 | } 18 | 19 | /** 20 | * Determines how to render content for the different kinds of fields 21 | * https://www.contentful.com/developers/docs/references/content-management-api/#/reference/content-types 22 | */ 23 | function renderContent (content, definition, location) { 24 | const {type, linkType} = definition 25 | if (typeof content === 'undefined' || content === null) { 26 | return

    No content

    27 | } else if (type === 'Link' && linkType === 'Entry' && content.sys.type === 'Entry') { 28 | return renderEntryLink(content, location) 29 | } else if (type === 'Link' && linkType === 'Asset' && content.sys.type === 'Asset') { 30 | return renderAssetLink(content, location) 31 | } else if (type === 'Link' && linkType === 'Entry' && content.sys.type === 'Link') { 32 | return

    Link to {content.sys.id} is missing.

    33 | } else if (type === 'Link' && linkType === 'Asset' && content.sys.type === 'Link') { 34 | return 35 | } else if (type === 'Array' && Array.isArray(content)) { 36 | return renderList(content, definition.items, location) 37 | } else if (type === 'Location' && isLocation(content)) { 38 | return renderLocation(content) 39 | } else if (type === 'Date') { 40 | return

    {formatDate(content)}

    41 | } else if (type === 'Object') { 42 | return renderObject(content) 43 | } else if (type === 'Boolean') { 44 | return renderBoolean(content) 45 | } else if (type === 'Text') { 46 | return

    47 | } else if (content.sys || content.fields) { 48 | return

    49 |

    Error rendering field {definition.id} with content:

    50 | {renderObject(content)} 51 |
    52 | } else { 53 | return

    {content}

    54 | } 55 | } 56 | 57 | function renderMarkdown (content) { 58 | return { 59 | __html: marked(removeIvalidDataURL(content), {sanitize: true}) 60 | } 61 | } 62 | function removeIvalidDataURL (content) { 63 | let regex = /data:\S+;base64\S*/gm 64 | return content.replace(regex, '#') 65 | } 66 | function renderEntryLink (content, location) { 67 | return 68 | } 69 | 70 | function renderAssetLink (content, location) { 71 | return 72 | 73 | 74 | } 75 | 76 | function renderList (list, definition, location) { 77 | const listStyle = determineListStyle(definition) 78 | const items = list.map((item, idx) => { 79 | return
    80 | {renderContent(item, definition, location)} 81 |
    82 | }) 83 | return
    {items}
    84 | } 85 | 86 | function determineListStyle (definition) { 87 | if (definition.type === 'Link') { 88 | if (definition.linkType === 'Entry') { 89 | return 'entries' 90 | } else if (definition.linkType === 'Asset') { 91 | return 'asset' 92 | } 93 | } else if (definition.type === 'Symbol') { 94 | return 'symbol' 95 | } 96 | } 97 | 98 | function renderLocation (content) { 99 | return ( 100 |
    101 |

    Latitude: {content.lat}

    102 |

    Longitude: {content.lon}

    103 |
    104 | ) 105 | } 106 | 107 | function renderObject (content) { 108 | const stringified = content.stringifySafe ? content.stringifySafe(null, ' ') : JSON.stringify(content, null, ' ') 109 | return
    {stringified}
    110 | } 111 | 112 | function renderBoolean (content) { 113 | return content ? 'Yes' : 'No' 114 | } 115 | 116 | function isLocation (obj) { 117 | return obj && obj.lat && obj.lon 118 | } 119 | 120 | export default CSSModules(Field, styles) 121 | -------------------------------------------------------------------------------- /src/components/settings/SettingsForm.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import CSSModules from 'react-css-modules' 3 | import { Link } from 'react-router' 4 | import styles from './Settings.css' 5 | 6 | const sourceCodeURL = 'https://github.com/contentful/discovery-app-react' 7 | 8 | class SettingsForm extends React.Component { 9 | 10 | constructor () { 11 | super() 12 | this.state = {detailsDisplay: {display: 'none'}} 13 | } 14 | 15 | toggleDetails (e) { 16 | e.preventDefault() 17 | const {detailsDisplay} = this.state 18 | const newDetailsDisplay = {display: detailsDisplay.display === 'none' ? 'block' : 'none'} 19 | this.setState({detailsDisplay: newDetailsDisplay}) 20 | } 21 | render () { 22 | return ( 23 |
    24 |
    25 |
    26 |

    27 | {this.props.validationError} 28 |

    29 |
    30 |

    Access any Space with Space ID and API Key

    31 | Where do I find Space ID and Delivery API Key 32 | 33 |
    34 |
      35 |
    1. 36 | Log into your Contentful account 37 |
    2. 38 |
    3. 39 | Select APIs on the top navigation bar 40 |
    4. 41 |
    5. 42 | Select API Keys on the left menu 43 |
    6. 44 |
    7. 45 | Select Website Key or create a new one 46 |
    8. 47 |
    9. 48 | Copy your Production and Preview keys 49 |
    10. 50 |
    51 | 52 |
    53 |
    54 |
    55 | 58 | 64 |
    65 |
    66 | 69 | 74 |
    75 |
    76 | 79 | 84 |
    85 | 88 |
    89 |
    90 |
    91 |
    92 |

    What is The Contentful Discovery App?

    93 |

    94 | The Contentful Discovery App gives you a quick and easy way to preview your content on a web environment, and explore the contents of your Spaces 95 |

    96 |
    97 |
    98 |

    Still don't have a Space?

    99 |

    100 | Load a demo Space 101 | or Create an account 102 |

    103 |
    104 |
    105 |

    What is Contentful

    106 |

    107 | Contentful is a content management platform for web applications, mobile apps and connected devices.It allows you to 108 | create, edit & manage content in the cloud and publish it anywhere via a powerful API.You can get your Space ID and Access Token from the API section of 109 | the Contentful Web App 110 |

    111 |
    112 |
    113 |

    This application is open source

    114 |

    115 | This application is Open Source. You can check out the source code, see how it was built, and suggest your own improvements. 116 |

    117 |
    118 |
    119 |
    120 | ) 121 | } 122 | } 123 | 124 | SettingsForm.propTypes = { 125 | space: PropTypes.string.isRequired, 126 | selectedAccessToken: PropTypes.string.isRequired, 127 | deliveryAccessToken: PropTypes.string.isRequired, 128 | previewAccessToken: PropTypes.string.isRequired, 129 | selectedApi: PropTypes.string.isRequired, 130 | handleChange: PropTypes.func.isRequired, 131 | loadSpace: PropTypes.func.isRequired, 132 | validationError: PropTypes.string 133 | } 134 | 135 | export default CSSModules(SettingsForm, styles) 136 | --------------------------------------------------------------------------------