├── .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
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 |
15 |
17 |
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 |
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
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 |
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 |
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 |
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 |
33 |
34 |
35 |
36 |
Discovery App
37 |
38 |
39 |
40 |
41 |
42 |
47 |
48 |
49 |
55 |
56 |
57 |
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 |
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 |
--------------------------------------------------------------------------------