├── .gitignore ├── src ├── img │ ├── contentful.png │ └── cosmic.svg ├── style.css ├── cosmic.service.js ├── importer.service.js ├── index.js ├── contentful.service.js └── contentful-sample.json ├── public └── index.html ├── extension.json ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | extension.zip -------------------------------------------------------------------------------- /src/img/contentful.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosmicjs/contentful-importer/master/src/img/contentful.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 |
-------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Contentful Importer", 3 | "font_awesome_class": "fa-download", 4 | "image_url": "https://cdn.cosmicjs.com/2d015110-41f7-11ea-93cf-dfe709ea319d-cosmic-contentful.png", 5 | "repo_url": "https://github.com/cosmicjs/contentful-importer" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | # Contentful to Cosmic Importer
4 |
5 | Import your Contentful space into Cosmic JS. Read about [how it was built here](https://www.cosmicjs.com/articles/creating-the-cosmic-js-contentful-importer-k5qwyunz).
6 |
7 | ## Installation
8 |
9 | Go to Your Bucket > Settings > Extensions to find and install this Extension. Also available [here](https://www.cosmicjs.com/extensions/contentful-importer).
10 |
11 | To run locally:
12 |
13 | ```
14 | npm install
15 | npm start
16 | ```
17 |
18 | ## Building
19 |
20 | ```
21 | npm run build
22 | ```
23 |
24 | ## Deploy
25 | Runs a build and compress to zip. The zip will then need to be uploaded to the Extension area in your Bucket.
26 |
27 | ```
28 | npm run deploy
29 | ```
30 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react",
3 | "version": "0.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "@contentful/rich-text-html-renderer": "^13.4.0",
7 | "cosmicjs": "^3.2.40",
8 | "pluralize": "^8.0.0",
9 | "prop-types": "^15.6.0",
10 | "react": "^16.9.0",
11 | "react-dom": "^16.9.0",
12 | "react-loading": "^2.0.3",
13 | "semantic-ui-react": "^0.88.2",
14 | "showdown": "^1.9.1"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "deploy": "npm run build && cp extension.json build/extension.json && zip -r build.zip build",
20 | "test": "react-scripts test --env=jsdom",
21 | "eject": "react-scripts eject"
22 | },
23 | "devDependencies": {
24 | "fs-extra": "^8.1.0",
25 | "react-scripts": "latest",
26 | "zip-a-folder": "0.0.10"
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | body,
2 | button,
3 | input {
4 | font-family: Lato;
5 | font-size: 15px;
6 | }
7 |
8 | button {
9 | padding: 5px 10px;
10 | }
11 |
12 | img {
13 | width: 300px;
14 | margin-right: 15px;
15 | }
16 |
17 | input {
18 | border: 1px solid grey;
19 | padding: 5px;
20 | }
21 |
22 | button,
23 | input {
24 | border-radius: 5px;
25 | }
26 |
27 | label {
28 | display: block;
29 | margin-bottom: 5px;
30 | }
31 |
32 | .root {
33 | background-color: #F5F7F9;
34 | height: 100%;
35 | padding: 20px 20px;
36 | }
37 | .objects-import-wrapper {
38 | position: absolute;
39 | width: 200px;
40 | height: 270px;
41 | top: -217px;
42 | overflow: hidden;
43 | left: -5px;
44 | }
45 | .objects-import {
46 | left: 0;
47 | transform: rotate(-90deg);
48 | z-index: 0;
49 | }
50 | .objects-import path {
51 | stroke-width: 7px;
52 | stroke-linecap: round;
53 | stroke-dasharray: 0 20;
54 | -webkit-animation: stroke-invert .8s linear infinite;
55 | animation: stroke-invert .8s linear infinite;
56 | }
57 | // Animation
58 | @keyframes stroke{to{stroke-dashoffset:20}}@-webkit-keyframes stroke-invert{to{stroke-dashoffset:-20}}@keyframes stroke-invert{to{stroke-dashoffset:-20}}
59 | .card-floating {
60 | box-shadow: rgba(0,21,64,.14) 0 2px 6px, rgba(0,21,64,.05) 0 10px 20px;
61 | border-radius: 200px;
62 | border: 14px solid #fff;
63 | background: #fff;
64 | z-index: 1;
65 | position: relative;
66 | padding: 20px;
67 | }
--------------------------------------------------------------------------------
/src/cosmic.service.js:
--------------------------------------------------------------------------------
1 | import CosmicFactory from "cosmicjs";
2 |
3 | export class CosmicService {
4 | bucket;
5 |
6 | constructor(slug, read_key, write_key) {
7 | const Cosmic = CosmicFactory();
8 |
9 | this.bucket = Cosmic.bucket({
10 | slug,
11 | read_key,
12 | write_key
13 | });
14 | }
15 |
16 | addMediaObjects(media) {
17 | try {
18 | const promises = media.map(media => this.addMediaObject(media));
19 | return Promise.all(promises);
20 | } catch (e) {
21 | console.log("caught at add media object", e);
22 |
23 | throw e;
24 | }
25 | }
26 |
27 | addMediaObject(params) {
28 | return new Promise((resolve, reject) => {
29 | this.bucket
30 | .addMedia(params)
31 | .then(data => {
32 | resolve(data);
33 | })
34 | .catch(err => {
35 | resolve({ failed: true, e: err, file: params });
36 | });
37 | });
38 | }
39 |
40 | addObjects(objectArr) {
41 | const promises = objectArr.map(object => this.addObject(object));
42 |
43 | return Promise.all(promises);
44 | }
45 |
46 | addObject(params) {
47 | return new Promise((resolve, reject) => {
48 | this.bucket
49 | .addObject(params)
50 | .then(data => resolve(data))
51 | .catch(err => {
52 | if (err.message && !err.message.includes("already exists")) {
53 | reject(err);
54 | }
55 |
56 | resolve(err);
57 | });
58 | });
59 | }
60 |
61 | addObjectTypes(typeArr) {
62 | const promises = typeArr.map(type => this.addObjectType(type));
63 |
64 | return Promise.all(promises);
65 | }
66 |
67 | addObjectType(params) {
68 | return new Promise((resolve, reject) => {
69 | this.bucket
70 | .addObjectType(params)
71 | .then(data => resolve(data))
72 | .catch(err => {
73 | if (err.message && !err.message.includes("already exists")) {
74 | reject(err);
75 | }
76 |
77 | resolve(err);
78 | });
79 | });
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/img/cosmic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
32 |
--------------------------------------------------------------------------------
/src/importer.service.js:
--------------------------------------------------------------------------------
1 | export class ImporterService {
2 | contentful;
3 | cosmic;
4 |
5 | constructor(contentfulService, cosmicService) {
6 | this.contentful = contentfulService;
7 | this.cosmic = cosmicService;
8 | }
9 |
10 | loadContentfulContent(file, onProgress, onError, onComplete, onMessage) {
11 | const reader = new FileReader();
12 |
13 | reader.readAsText(file, "UTF-8");
14 |
15 | reader.onload = e => {
16 | try {
17 | const content = e.target.result;
18 |
19 | const json = JSON.parse(content);
20 |
21 | this._parseContent(json, onProgress, onError, onComplete, onMessage);
22 | } catch (e) {
23 | onError(e);
24 | }
25 | };
26 | }
27 |
28 | async _parseContent(content, onProgress, onError, onComplete, onMessage) {
29 | try {
30 | this._validateContent(content);
31 |
32 | onProgress("Content valid. Parsing...");
33 |
34 | const fields = this.contentful.toCosmicObjectTypes(content.contentTypes);
35 |
36 | onProgress("Successfully parsed content types");
37 |
38 | await this.cosmic.addObjectTypes(fields.contentTypes);
39 |
40 | onProgress("Successfully created content types");
41 |
42 | const media = (
43 | await this.contentful.toCosmicMedia(content.assets, content.locales)
44 | ).filter(mediaObject => {
45 | if (!mediaObject) {
46 | return false;
47 | } else if (mediaObject.failed) {
48 | onMessage(
49 | `Failed to download image from contentful: ${mediaObject.title}`
50 | );
51 |
52 | return false;
53 | }
54 |
55 | return true;
56 | });
57 |
58 | onProgress("Successfully parsed media");
59 |
60 | onProgress("Uploading media to Cosmic...");
61 |
62 | const cosmicMedia = await this.cosmic.addMediaObjects(media);
63 |
64 | const successfulMedia = cosmicMedia.filter(media => {
65 | if (media.failed) {
66 | onMessage(
67 | `Failed to upload image: ${media.file.metadata.title} - ${media.file.metadata.originalUrl}`
68 | );
69 |
70 | return false;
71 | }
72 |
73 | return true;
74 | });
75 |
76 | onProgress("Successfully created media");
77 |
78 | const parsedObjects = this.contentful.toCosmicObjects(
79 | content.entries,
80 | content.locales,
81 | fields.displayFieldMap,
82 | fields.metafieldDescriptors,
83 | successfulMedia
84 | );
85 |
86 | onProgress("Successfully parsed entries");
87 |
88 | await this.cosmic.addObjects(parsedObjects);
89 |
90 | onProgress("Successfully created objects");
91 |
92 | onComplete();
93 | } catch (e) {
94 | onError(e);
95 | console.log(e);
96 | }
97 | }
98 |
99 | _validateContent(content) {
100 | if (
101 | !content ||
102 | !content.contentTypes ||
103 | !content.entries ||
104 | !content.locales
105 | ) {
106 | throw new Error("invalid content");
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { render } from "react-dom";
3 | import { CosmicService } from "./cosmic.service";
4 | import { ContentfulService } from "./contentful.service";
5 | import { ImporterService } from "./importer.service";
6 | import ReactLoading from "react-loading";
7 | import "./style.css";
8 | import { Input, Button, Modal, Header, Icon, Message } from 'semantic-ui-react'
9 |
10 | import cosmicLogo from "./img/cosmic.svg";
11 | import contentfulLogo from "./img/contentful.png";
12 |
13 | const getParam = param => {
14 | var urlParams = new URLSearchParams(window.location.search);
15 | return urlParams.get(param);
16 | };
17 |
18 | class App extends Component {
19 | importerService;
20 |
21 | constructor() {
22 | super();
23 |
24 | this.state = {
25 | file: null,
26 | slug: getParam("bucket_slug"),
27 | read_key: getParam("read_key"),
28 | write_key: getParam("write_key"),
29 | errorMessage: false,
30 | progress: false,
31 | loading: false,
32 | messages: []
33 | };
34 |
35 | this.createService();
36 |
37 | this.closeSuccessModal = () => {
38 | this.setState({
39 | ...this.state,
40 | progress: false
41 | });
42 | }
43 | }
44 |
45 |
46 | render() {
47 | const { errorMessage, loading, progress, messages, slug } = this.state;
48 | if (progress === 'Successfully created objects') {
49 | return (
50 |
54 | Your entries have been successfully imported! Go see them
79 | To import data from Contentful create an export file via the 80 | Contentful CLI then upload it here. 81 |
82 | Use the Contentful CLI to download a JSON file export of your space. Example: 83 |
85 | contentful space export --space-id YOUR_SPACE_ID --management-token YOUR_MANAGEMENT_TOKEN
86 |
87 | 89 | For more information follow the instructions here. 90 |
91 |