├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bundlesize.config.json ├── docs ├── 01-index.md ├── 02-api.md ├── 03-frontend.md ├── 04-architecture.md ├── 05-development.md ├── _config.yml ├── _frontend │ ├── actions.md │ ├── components.md │ ├── constants.md │ ├── containers.md │ ├── reducers.md │ └── store.md ├── _includes │ ├── head.html │ └── sidebar.html ├── _layouts │ ├── default.html │ └── page.html ├── _sass │ ├── content.scss │ ├── header.scss │ ├── mixins.scss │ ├── normalize.scss │ ├── sidebar.scss │ ├── syntax-highlighting.scss │ └── variables.scss └── assets │ ├── atom-one-dark.min.css │ ├── favicon.ico │ ├── highlight.min.js │ ├── images │ └── logo.png │ └── main.scss ├── jekyll-manager.gemspec ├── lib ├── jekyll-admin │ ├── apiable.rb │ ├── data_file.rb │ ├── directory.rb │ ├── file_helper.rb │ ├── path_helper.rb │ ├── server.rb │ ├── server │ │ ├── collection.rb │ │ ├── configuration.rb │ │ ├── dashboard.rb │ │ ├── data.rb │ │ ├── draft.rb │ │ ├── page.rb │ │ ├── static_file.rb │ │ ├── template.rb │ │ └── theme.rb │ ├── static_server.rb │ └── urlable.rb ├── jekyll-manager.rb ├── jekyll-manager │ └── version.rb └── jekyll │ └── commands │ ├── build.rb │ └── serve.rb ├── package.json ├── screenshot.png ├── script ├── bootstrap ├── branding ├── build ├── cibuild ├── cibuild-node ├── cibuild-ruby ├── docs-server ├── fmt ├── release ├── server-frontend └── test-server ├── spec ├── fixtures │ ├── index.html │ ├── site │ │ ├── _config.yml │ │ ├── _config_test.yml │ │ ├── _data │ │ │ ├── data_file.yml │ │ │ ├── members.csv │ │ │ ├── members.tsv │ │ │ ├── movies │ │ │ │ ├── actors.yml │ │ │ │ └── genres │ │ │ │ │ └── fiction.yml │ │ │ ├── plain_text.txt │ │ │ ├── settings.json │ │ │ └── template-config.yaml │ │ ├── _drafts │ │ │ ├── draft-dir │ │ │ │ ├── WIP │ │ │ │ │ └── yet-another-draft-post.md │ │ │ │ └── another-draft-post.md │ │ │ ├── draft-post.md │ │ │ └── random │ │ │ │ └── notes.txt │ │ ├── _layouts │ │ │ ├── default.html │ │ │ └── page.html │ │ ├── _posts │ │ │ ├── 2016-01-01-test-post.md │ │ │ ├── 2016-02-01-test-post-2.md │ │ │ ├── 2016-03-01-test-post-3.md │ │ │ ├── more posts │ │ │ │ ├── 2016-04-01-post-within-subdirectory.md │ │ │ │ └── some more posts │ │ │ │ │ ├── 2016-05-01-a-test-post-within-subdirectory.md │ │ │ │ │ └── 2016-05-02-another-test-post-within-subdirectory.md │ │ │ └── test │ │ │ │ └── 2016-01-02-test2.md │ │ ├── _puppies │ │ │ └── rover.md │ │ ├── _sass │ │ │ ├── main.scss │ │ │ └── test-dir │ │ │ │ └── _base.scss │ │ ├── assets │ │ │ ├── app.coffee │ │ │ ├── images │ │ │ │ ├── icon-github.svg │ │ │ │ └── jekyll-logo-light-solid.png │ │ │ ├── scripts │ │ │ │ └── script.js │ │ │ ├── static-file.txt │ │ │ └── style.scss │ │ ├── index.html │ │ ├── page-dir │ │ │ ├── page1.md │ │ │ └── test │ │ │ │ └── page2.md │ │ ├── page.md │ │ └── static-file.txt │ └── test-theme │ │ ├── LICENSE │ │ ├── _includes │ │ └── include.html │ │ ├── _layouts │ │ ├── default.html │ │ └── page.html │ │ ├── _sass │ │ ├── test-theme-black.scss │ │ └── test-theme-red.scss │ │ ├── assets │ │ ├── application.coffee │ │ ├── images │ │ │ └── icon-dark.png │ │ └── style.scss │ │ └── test-theme.gemspec ├── jekyll-admin │ ├── apiable_spec.rb │ ├── custom_integration_spec.rb │ ├── data_file_spec.rb │ ├── file_helper_spec.rb │ ├── integration_spec.rb │ ├── path_helper_spec.rb │ ├── server │ │ ├── collection_spec.rb │ │ ├── configuration_spec.rb │ │ ├── dashboard_spec.rb │ │ ├── data_spec.rb │ │ ├── draft_spec.rb │ │ ├── page_spec.rb │ │ ├── static_file_spec.rb │ │ ├── template_spec.rb │ │ └── theme_spec.rb │ ├── server_spec.rb │ ├── static_server_spec.rb │ └── urlable_spec.rb ├── jekyll_admin_spec.rb └── spec_helper.rb ├── src ├── assets │ ├── favicon.ico │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ ├── lato-bold.ttf │ │ └── lato-regular.ttf │ └── images │ │ ├── logo-black-red.png │ │ ├── logo.png │ │ └── no-image.svg ├── components │ ├── Breadcrumbs.js │ ├── Button.js │ ├── Collapsible.js │ ├── Dropzone.js │ ├── Editor.js │ ├── Errors.js │ ├── FilePreview.js │ ├── MarkdownEditor.js │ ├── Splitter.js │ ├── Toggled.js │ ├── dashboard │ │ ├── Gauge.js │ │ └── GaugeBoard.js │ ├── form │ │ ├── Checkbox.js │ │ ├── InputPath.js │ │ ├── InputSearch.js │ │ ├── InputTitle.js │ │ └── tests │ │ │ ├── checkbox.spec.js │ │ │ ├── inputpath.spec.js │ │ │ ├── inputsearch.spec.js │ │ │ └── inputtitle.spec.js │ ├── metadata │ │ ├── MetaArray.js │ │ ├── MetaArrayItem.js │ │ ├── MetaButtons.js │ │ ├── MetaField.js │ │ ├── MetaObject.js │ │ ├── MetaObjectItem.js │ │ ├── MetaSimple.js │ │ ├── MetaTags.js │ │ └── tests │ │ │ ├── metaarray.spec.js │ │ │ ├── metaarrayitem.spec.js │ │ │ ├── metabuttons.spec.js │ │ │ ├── metafield.spec.js │ │ │ ├── metaobject.spec.js │ │ │ ├── metaobjectitem.spec.js │ │ │ ├── metasimple.spec.js │ │ │ └── metatags.spec.js │ └── tests │ │ ├── breadcrumbs.spec.js │ │ ├── button.spec.js │ │ ├── collapsible.spec.js │ │ ├── editor.spec.js │ │ ├── errors.spec.js │ │ ├── filepreview.spec.js │ │ └── fixtures │ │ └── index.js ├── constants │ ├── api.js │ ├── index.js │ ├── keyboardShortcuts.js │ └── lang │ │ ├── da.js │ │ ├── en.js │ │ ├── fr.js │ │ ├── index.js │ │ └── sv.js ├── containers │ ├── App.js │ ├── Header.js │ ├── MetaFields.js │ ├── Notifications.js │ ├── Sidebar.js │ ├── tests │ │ ├── __snapshots__ │ │ │ ├── header.spec.js.snap │ │ │ ├── notifications.spec.js.snap │ │ │ └── sidebar.spec.js.snap │ │ ├── fixtures │ │ │ └── index.js │ │ ├── header.spec.js │ │ ├── metafields.spec.js │ │ ├── notifications.spec.js │ │ └── sidebar.spec.js │ └── views │ │ ├── Configuration.js │ │ ├── Dashboard.js │ │ ├── DataFileEdit.js │ │ ├── DataFileNew.js │ │ ├── DataFiles.js │ │ ├── DocumentEdit.js │ │ ├── DocumentNew.js │ │ ├── Documents.js │ │ ├── DraftEdit.js │ │ ├── DraftNew.js │ │ ├── Drafts.js │ │ ├── NotFound.js │ │ ├── PageEdit.js │ │ ├── PageNew.js │ │ ├── Pages.js │ │ ├── StaticFiles.js │ │ ├── StaticIndex.js │ │ ├── TemplateDirectory.js │ │ ├── TemplateEdit.js │ │ ├── TemplateNew.js │ │ ├── TemplateView.js │ │ ├── Templates.js │ │ ├── ThemeDirectory.js │ │ ├── ThemeManifest.js │ │ └── tests │ │ ├── configuration.spec.js │ │ ├── datafileedit.spec.js │ │ ├── datafilenew.spec.js │ │ ├── datafiles.spec.js │ │ ├── documentedit.spec.js │ │ ├── documentnew.spec.js │ │ ├── documents.spec.js │ │ ├── draftedit.spec.js │ │ ├── drafts.spec.js │ │ ├── fixtures │ │ └── index.js │ │ ├── pageedit.spec.js │ │ ├── pagenew.spec.js │ │ ├── pages.spec.js │ │ ├── staticfiles.spec.js │ │ └── staticindex.spec.js ├── ducks │ ├── action_tests │ │ ├── collections.spec.js │ │ ├── config.spec.js │ │ ├── dashboard.spec.js │ │ ├── datafiles.spec.js │ │ ├── drafts.spec.js │ │ ├── fixtures │ │ │ └── index.js │ │ ├── metadata.spec.js │ │ ├── notifications.spec.js │ │ ├── pages.spec.js │ │ ├── staticfiles.spec.js │ │ ├── templates.spec.js │ │ ├── theme.spec.js │ │ └── utils.spec.js │ ├── collections.js │ ├── config.js │ ├── dashboard.js │ ├── datafiles.js │ ├── drafts.js │ ├── index.js │ ├── metadata.js │ ├── notifications.js │ ├── pages.js │ ├── reducer_tests │ │ ├── collections.spec.js │ │ ├── config.spec.js │ │ ├── dashboard.spec.js │ │ ├── datafiles.spec.js │ │ ├── drafts.spec.js │ │ ├── fixtures │ │ │ └── index.js │ │ ├── metadata.spec.js │ │ ├── notifications.spec.js │ │ ├── pages.spec.js │ │ ├── staticfiles.spec.js │ │ ├── templates.spec.js │ │ ├── theme.spec.js │ │ └── utils.spec.js │ ├── staticfiles.js │ ├── templates.js │ ├── theme.js │ └── utils.js ├── index.html ├── index.js ├── routes.js ├── store │ └── configureStore.js ├── styles │ ├── _checkbox.scss │ ├── breadcrumbs.scss │ ├── button.scss │ ├── collapsible.scss │ ├── content.scss │ ├── dashboard.scss │ ├── datagui.scss │ ├── editor.scss │ ├── form.scss │ ├── header.scss │ ├── main.scss │ ├── metafields.scss │ ├── mixins.scss │ ├── normalize.css │ ├── sidebar.scss │ ├── staticfiles.scss │ ├── theme.scss │ ├── toggled.scss │ ├── variables.scss │ └── vendors │ │ ├── codemirror.scss │ │ └── font-awesome │ │ ├── _animated.scss │ │ ├── _bordered-pulled.scss │ │ ├── _core.scss │ │ ├── _fixed-width.scss │ │ ├── _icons.scss │ │ ├── _larger.scss │ │ ├── _list.scss │ │ ├── _mixins.scss │ │ ├── _path.scss │ │ ├── _rotated-flipped.scss │ │ ├── _screen-reader.scss │ │ ├── _stacked.scss │ │ ├── _variables.scss │ │ └── font-awesome.scss └── utils │ ├── api_errors.js │ ├── fetch.js │ ├── helpers.js │ ├── metadata.js │ ├── tests │ ├── fixtures │ │ └── index.js │ ├── helpers.spec.js │ ├── metadata.spec.js │ └── validation.spec.js │ └── validation.js ├── tools ├── build.js ├── buildHtml.js ├── chalkConfig.js ├── fileMock.js ├── srcServer.js ├── startMessage.js └── testCi.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": { 4 | "presets": ["env", "react", "react-hmre", "stage-1"] 5 | }, 6 | "production": { 7 | "presets": [ 8 | [ 9 | "env", 10 | { 11 | "es2015": { 12 | "modules": false 13 | }, 14 | "targets": { 15 | "ie": 9, 16 | "uglify": true 17 | } 18 | } 19 | ], 20 | "react", 21 | "stage-1" 22 | ], 23 | "plugins": [ 24 | "transform-react-constant-elements", 25 | "transform-react-remove-prop-types" 26 | ] 27 | }, 28 | "test": { 29 | "presets": ["env", "react", "stage-1"] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:import/errors", 6 | "plugin:import/warnings" 7 | ], 8 | "plugins": ["react"], 9 | "parser": "babel-eslint", 10 | "parserOptions": { 11 | "ecmaVersion": 6, 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true, 15 | "experimentalObjectRestSpread": true 16 | } 17 | }, 18 | "env": { 19 | "es6": true, 20 | "browser": true, 21 | "node": true, 22 | "jquery": true, 23 | "jest": true 24 | }, 25 | "rules": { 26 | "import/no-named-as-default": 0, 27 | "quotes": 0, 28 | "no-console": 1, 29 | "no-unused-vars": [1, { "args": "none" }], 30 | "no-debugger": 1, 31 | "no-var": 1, 32 | "object-shorthand": [1, "properties"], 33 | "semi": [1, "always"], 34 | "no-trailing-spaces": 0, 35 | "eol-last": 0, 36 | "no-underscore-dangle": 0, 37 | "no-alert": 0, 38 | "no-lone-blocks": 0, 39 | "jsx-quotes": 1, 40 | "react/display-name": [ 41 | 1, 42 | { 43 | "ignoreTranspilerName": false 44 | } 45 | ], 46 | "react/forbid-prop-types": 0, 47 | "react/jsx-boolean-value": 0, 48 | "react/jsx-closing-bracket-location": 0, 49 | "react/jsx-curly-spacing": 1, 50 | "react/jsx-indent-props": 0, 51 | "react/jsx-key": 1, 52 | "react/jsx-max-props-per-line": 0, 53 | "react/jsx-no-bind": 0, 54 | "react/jsx-no-duplicate-props": 1, 55 | "react/jsx-no-literals": 0, 56 | "react/jsx-no-undef": 1, 57 | "react/jsx-pascal-case": 1, 58 | "react/jsx-sort-prop-types": 0, 59 | "react/jsx-sort-props": 0, 60 | "react/jsx-uses-react": 1, 61 | "react/jsx-uses-vars": 1, 62 | "react/jsx-wrap-multilines": 1, 63 | "react/no-danger": 1, 64 | "react/no-did-mount-set-state": 1, 65 | "react/no-did-update-set-state": 1, 66 | "react/no-direct-mutation-state": 1, 67 | "react/no-multi-comp": 1, 68 | "react/no-set-state": 0, 69 | "react/no-unknown-property": 1, 70 | "react/prefer-es6-class": 1, 71 | "react/prop-types": 1, 72 | "react/react-in-jsx-scope": 1, 73 | "import/extensions": 1, 74 | "react/self-closing-comp": 1, 75 | "react/sort-comp": 1 76 | }, 77 | "globals": { 78 | "VERSION": true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ### Description: 5 | 6 | 7 | 8 | ### Tell us a bit about yourself: 9 | * Version of Jekyll Manager I'm using : 10 | * Version of Jekyll I'm using : 11 | * Version of NodeJS I'm using : 12 | * Operating System : 13 | * Browser : 14 | 15 | ### Steps to reproduce: 16 | 17 | 18 | 19 | ### I expected the following: 20 | 21 | 22 | 23 | ### But got the following, instead: 24 | 25 | 26 | 27 | ### Other details: 28 | 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | ruby: 11 | name: "Test Backend (Jekyll ${{ matrix.jekyll_version }}, Ruby ${{ matrix.ruby_version }})" 12 | runs-on: "ubuntu-latest" 13 | env: 14 | JEKYLL_VERSION: ${{ matrix.jekyll_version }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | ruby_version: ["2.7"] 19 | jekyll_version: 20 | - "~> 3.9" 21 | - "~> 4.0" 22 | steps: 23 | - name: Checkout Repository 24 | uses: actions/checkout@v4 25 | - name: "Set up Ruby ${{ matrix.ruby_version }}" 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby_version }} 29 | bundler-cache: true 30 | - name: Unit Tests 31 | run: bundle exec rspec 32 | 33 | node: 34 | name: "Test Frontend (Node ${{ matrix.node_version }})" 35 | runs-on: "ubuntu-latest" 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | node_version: ["12"] 40 | steps: 41 | - name: Checkout Repository 42 | uses: actions/checkout@v4 43 | - name: "Set up Node ${{ matrix.node_version }}" 44 | uses: actions/setup-node@v2 45 | with: 46 | node-version: ${{ matrix.node_version }} 47 | cache: "yarn" 48 | - name: Install Dependencies 49 | run: yarn install 50 | - name: Build frontend and run tests 51 | run: bash script/build 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _site/ 2 | .gems/ 3 | .jekyll-cache/ 4 | .sass-cache/ 5 | .jekyll-metadata 6 | /.bundle/ 7 | /.yardoc 8 | /Gemfile.lock 9 | /_yardoc/ 10 | /coverage/ 11 | /doc/ 12 | /pkg/ 13 | /spec/reports/ 14 | /tmp/ 15 | /spec/fixtures/site/_site 16 | npm-debug.log* 17 | /spec/fixtures/site/Gemfile.lock 18 | node_modules 19 | /lib/jekyll-admin/public 20 | *.gem 21 | .DS_Store 22 | report.html 23 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-jekyll 2 | inherit_gem: 3 | rubocop-jekyll: '.rubocop.yml' 4 | 5 | AllCops: 6 | Exclude: 7 | - lib/jekyll-admin/public/**/* 8 | - src/**/* 9 | - node_modules/**/* 10 | - vendor/**/* 11 | 12 | Metrics/BlockLength: 13 | Enabled: false 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in jekyll-manager.gemspec 6 | gemspec 7 | 8 | # To allow testing with specific Jekyll versions 9 | gem "jekyll", ENV["JEKYLL_VERSION"] if ENV["JEKYLL_VERSION"] 10 | gem "kramdown-parser-gfm" if ENV["JEKYLL_VERSION"] == "~> 3.9" 11 | 12 | # Site dependencies 13 | gem "jekyll-seo-tag" 14 | gem "jekyll-sitemap" 15 | 16 | # theme 17 | gem "test-theme", :path => "spec/fixtures/test-theme" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016-2017 Mert Kahyaoğlu and the Jekyll Admin contributors 4 | Copyright 2017 Ashwin Maroli 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | task :default => :spec 5 | -------------------------------------------------------------------------------- /bundlesize.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "path": "lib/jekyll-admin/public/bundle.js.map", 5 | "maxSize": "14 MB", 6 | "compression": "none" 7 | }, 8 | { 9 | "path": "lib/jekyll-admin/public/bundle.js", 10 | "maxSize": "4 MB", 11 | "compression": "none" 12 | }, 13 | { 14 | "path": "lib/jekyll-admin/public/styles.css.map", 15 | "maxSize": "200 kB", 16 | "compression": "none" 17 | }, 18 | { 19 | "path": "lib/jekyll-admin/public/styles.css", 20 | "maxSize": "200 kB", 21 | "compression": "none" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /docs/01-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | permalink: / 4 | description: Site Description 5 | --- 6 | 7 | ## Installation 8 | 9 | Refer to [Install Plugins](https://jekyllrb.com/docs/plugins/#installing-a-plugin) in Jekyll docs and install the `jekyll-manager` plugin as you would normally by adding `jekyll-manager` to the `:jekyll_plugins` group in your `Gemfile` and running `bundle install`. 10 | 11 | ## Usage 12 | 13 | 1. Start Jekyll as you would normally (`bundle exec jekyll serve`) 14 | 2. Navigate to `http://localhost:4000/admin` to access the administrative interface 15 | 16 | ## Contributing 17 | 18 | Jekyll Manager is a fork of the official plugin Jekyll Admin with some [significant divergence](https://github.com/ashmaroli/jekyll-manager#divergence). 19 | Bug reports and pull requests to improve these changes or this documenation are welcome on GitHub at . 20 | 21 | ## Improve this portal 22 | 23 | Found a mistake? See something that can be made better? These docs are open source. 24 | -------------------------------------------------------------------------------- /docs/03-frontend.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Front End 3 | permalink: /frontend/ 4 | description: The Javascript-based front end of Jekyll Manager, built on the Ruby-based HTTP API. 5 | --- 6 | 7 | ## Tech Stack 8 | 9 | Jekyll Manager uses the following technology stack for the front end. 10 | 11 | * [React](https://facebook.github.io/react/) 12 | * [Redux](http://redux.js.org/) 13 | * [Webpack](https://github.com/webpack/webpack) 14 | * [SASS](https://github.com/sass/sass) 15 | -------------------------------------------------------------------------------- /docs/04-architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Architecture 3 | permalink: /architecture/ 4 | description: Jekyll Manager exists in two parts, a Ruby back end and a Javascript front end. The two halves communicate via a shared API. 5 | --- 6 | 7 | ## How Jekyll Manager hooks into Jekyll 8 | 9 | Jekyll Manager piggybacks on Jekyll's built-in Webrick server. We rely on the `JekyllAdmin` monkey patch to the `jekyll serve` command to hook in two Sinatra servers, one to serve the static front end that lives in `lib/jekyll-admin/public` via `/admin`, and one to serve the Ruby API via `/_api`. Once the Sinatra servers are mounted, they work just like any other Jekyll server (and we rely on third-party plugins like `sinatra-json` and `sinatra-cross_origin`). 10 | 11 | > **Note:** Since there are two Sinatra servers that might call `site.process` concurrently, Jekyll Manager disables `--watch` flag to prevent a race condition between these servers that might cause incorrect responses for the API. This ensures that the site is regenerated by only the process that Jekyll Manager runs. 12 | 13 | ## How Jekyll Manager formats Jekyll objects for the API 14 | 15 | Whenever possible, we want to stay as close to the `to_liquid.to_json` representation of Jekyll's internal object structure as possible, so that we're (A) using data structure that developers are used to, and (B) so that we can benefit from upstream improvements. To do this, we have the `JekyllAdmin::APIable` module, which gets included in native Jekyll objects like Pages and Documents to add a `to_api` method which we can call to generate hashes for use in the API. 16 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: Jekyll Manager 2 | description: A repackaged Jekyll Admin fork with some alterations. 3 | collections: 4 | frontend: 5 | permalink: /frontend/:path/ 6 | output: true 7 | 8 | gems: 9 | - jekyll-seo-tag 10 | - jekyll-sitemap 11 | 12 | sass: 13 | style: compressed 14 | 15 | defaults: 16 | - 17 | scope: 18 | path: "" 19 | values: 20 | layout: "page" 21 | 22 | baseurl: "/jekyll-manager" 23 | -------------------------------------------------------------------------------- /docs/_frontend/constants.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Constants 3 | --- 4 | 5 | ## Action Types 6 | 7 | Stores all of the action creators' names 8 | 9 | ## Lang 10 | 11 | Stores the language specific files. Selected language strings are exported from `index.js`. 12 | 13 | This allows us to control all of the language specific strings in a single place and also will help us localize the admin panel in the future. 14 | 15 | ## API 16 | 17 | Stores all of the API endpoints 18 | -------------------------------------------------------------------------------- /docs/_frontend/store.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Store 3 | description: Store is the object that brings actions and reducers together. 4 | --- 5 | 6 | ## Middlewares 7 | 8 | The store has the following middlewares; 9 | 10 | * [Redux Thunk](https://github.com/gaearon/redux-thunk) - allows you to write action creators that return a function instead of an action. 11 | * [Redux Logger](https://github.com/evgenyrodionov/redux-logger) - Logs fired actions to the console. 12 | -------------------------------------------------------------------------------- /docs/_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ site.title }} 6 | 7 | 8 | {% seo %} 9 | 10 | -------------------------------------------------------------------------------- /docs/_includes/sidebar.html: -------------------------------------------------------------------------------- 1 | {% assign active = page.url | downcase | split: '/' %} 2 | 3 | 31 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include head.html %} 4 | 5 |
6 |
7 | 8 | {% include sidebar.html %} 9 | 10 |
11 | 12 |
13 |

{{ site.title }} - {{ page.title | escape }}

14 | 21 |
22 | 23 |
24 | {{ content }} 25 |
26 | 27 |
28 |
29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 | {% if page.description %} 7 |

8 | {% if page.description == "Site Description" %} 9 | {{ site.description }} 10 | {% else %} 11 | {{ page.description }} 12 | {% endif %} 13 |

14 | {% endif %} 15 |
16 | {{ content }} 17 |
18 |
19 | -------------------------------------------------------------------------------- /docs/_sass/content.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .content { 4 | padding: 40px; 5 | line-height: 1.8; 6 | @media(max-width: 768px) { 7 | padding: 15px; 8 | } 9 | 10 | a { 11 | color: $dark-orange; 12 | font-weight: 700; 13 | &:hover { 14 | border-bottom: 1px dotted; 15 | } 16 | } 17 | 18 | .description { 19 | margin-top: 0; 20 | margin-bottom: 45px; 21 | padding: 15px; 22 | color: #454545; 23 | font-size: 16px; 24 | font-weight: 700; 25 | background-color: #fffbdb; 26 | border-left: 8px solid $dark-orange; 27 | } 28 | 29 | .content-header { 30 | display: flex; 31 | align-items: center; 32 | margin: 0 0 35px 0; 33 | h1 { 34 | margin: 0 40px 0 0; 35 | float: left; 36 | text-shadow: 1px 1px 1px rgba(68, 68, 68, 0.3); 37 | } 38 | .page-buttons { 39 | display: flex; 40 | } 41 | } 42 | 43 | .splitter { 44 | margin: 15px 0; 45 | background: #cfcfcf; 46 | border: 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/_sass/header.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins'; 2 | 3 | .header { 4 | display: flex; 5 | align-items: center; 6 | height: 75px; 7 | background-color: white; 8 | padding: 0 40px; 9 | h1 { 10 | color: #444444; 11 | font-size: 24px; 12 | font-weight: normal; 13 | } 14 | ul { 15 | display: inline-block; 16 | padding: 0; 17 | list-style: none; 18 | li { 19 | float: left; 20 | &:not(:last-child) { 21 | margin-right: 15px; 22 | } 23 | } 24 | } 25 | @media(max-width: 768px) { 26 | margin: auto; 27 | display: block; 28 | text-align: center; 29 | padding: 15px; 30 | height: auto; 31 | h1 { 32 | font-size: 20px; 33 | } 34 | } 35 | @include box-shadow(0 2px 4px 0 rgba(204, 204, 204, 0.4)); 36 | } 37 | -------------------------------------------------------------------------------- /docs/_sass/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin box-shadow($shadow...) { 2 | -webkit-box-shadow: $shadow; 3 | -moz-box-shadow: $shadow; 4 | box-shadow: $shadow; 5 | } 6 | 7 | @mixin box-sizing($sizing...) { 8 | -webkit-box-sizing: $sizing; 9 | -moz-box-sizing: $sizing; 10 | box-sizing: $sizing; 11 | } 12 | 13 | @mixin border-radius($radius...) { 14 | -webkit-border-radius: $radius; 15 | -moz-border-radius: $radius; 16 | border-radius: $radius; 17 | } 18 | 19 | @mixin border-top-left-radius($radius...) { 20 | -webkit-border-top-left-radius: $radius; 21 | -moz-border-radius-topleft: $radius; 22 | border-top-left-radius: $radius; 23 | } 24 | 25 | @mixin border-top-right-radius($radius...) { 26 | -webkit-border-top-right-radius: $radius; 27 | -moz-border-radius-topright: $radius; 28 | border-top-right-radius: $radius; 29 | } 30 | 31 | @mixin transition($transition...) { 32 | -webkit-transition: $transition; 33 | -moz-transition: $transition; 34 | -o-transition: $transition; 35 | transition: $transition; 36 | } 37 | -------------------------------------------------------------------------------- /docs/_sass/normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} 2 | -------------------------------------------------------------------------------- /docs/_sass/sidebar.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | .sidebar{ 4 | position: fixed; 5 | top: 0; 6 | bottom: 0; 7 | left: 0; 8 | width: $sidebar-width; 9 | overflow-y: auto; 10 | min-height: 100%; 11 | background-color: #333; 12 | @media(max-width: 768px) { 13 | position: static; 14 | width: 100%; 15 | .menu-icon { 16 | display: block !important; 17 | } 18 | } 19 | .menu-icon { 20 | position: absolute; 21 | right: 15px; 22 | top: 15px; 23 | padding: 15px; 24 | display: none; 25 | z-index: 100; 26 | } 27 | .logo { 28 | display: block; 29 | height: 75px; 30 | background: #222222 url(../assets/images/logo.png) no-repeat center; 31 | background-size: 110px 46px; 32 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.4); 33 | &:hover { 34 | -webkit-filter: brightness(1.2); 35 | filter: brightness(1.2); 36 | } 37 | } 38 | .routes { 39 | padding: 20px 0; 40 | list-style: none; 41 | .route-section { 42 | border: 1px solid; 43 | border-color: lighten($sidebar-color, 6%) lighten($sidebar-color, 3%) darken($sidebar-color, 7%) transparent; 44 | &:first-of-type { 45 | border-top: none; 46 | } 47 | &:last-of-type { 48 | border-bottom: none; 49 | } 50 | } 51 | li { 52 | overflow-y: hidden; 53 | @include transition(max-height 0.5s); 54 | a { 55 | display: block; 56 | padding: 10px 20px; 57 | font-size: 17px; 58 | color: #beb9b1; 59 | &.active { 60 | color: $dark-orange; 61 | } 62 | } 63 | &:hover { 64 | background-color: lighten($sidebar-color, 3%); 65 | } 66 | 67 | ul { 68 | padding: 0; 69 | list-style: none; 70 | 71 | li { 72 | background: darken($sidebar-color, 3%); 73 | border-top: 1px solid lighten($sidebar-color, 2%); 74 | border-bottom: 1px solid darken($sidebar-color, 8%); 75 | &:last-of-type { 76 | border-bottom-color: transparent; 77 | } 78 | &:hover { 79 | background: darken($sidebar-color, 6%); 80 | } 81 | 82 | a { padding-left: 40px; } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /docs/_sass/variables.scss: -------------------------------------------------------------------------------- 1 | $sidebar-width: 220px; 2 | $content-side-width: 260px; 3 | 4 | $orange: #ffcc00; 5 | $dark-orange: #ff9900; 6 | 7 | $background-color: #fefefe; 8 | $sidebar-color: #333; 9 | 10 | $border-color: darken($background-color, 15%); 11 | $border-color-focus: $orange; 12 | $border-radius: 2px; 13 | -------------------------------------------------------------------------------- /docs/assets/atom-one-dark.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-keyword,.hljs-formula{color:#c678dd}.hljs-section,.hljs-name,.hljs-selector-tag,.hljs-deletion,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-string,.hljs-regexp,.hljs-addition,.hljs-attribute,.hljs-meta-string{color:#98c379}.hljs-built_in,.hljs-class .hljs-title{color:#e6c07b}.hljs-attr,.hljs-variable,.hljs-template-variable,.hljs-type,.hljs-selector-class,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-number{color:#d19a66}.hljs-symbol,.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-title{color:#61aeee}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold}.hljs-link{text-decoration:underline} -------------------------------------------------------------------------------- /docs/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/docs/assets/favicon.ico -------------------------------------------------------------------------------- /docs/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/docs/assets/images/logo.png -------------------------------------------------------------------------------- /docs/assets/main.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | @import 'normalize'; 4 | @import 'variables'; 5 | @import 'mixins'; 6 | @import 'sidebar'; 7 | @import 'header'; 8 | @import 'content'; 9 | @import 'syntax-highlighting'; 10 | 11 | * { 12 | box-sizing: border-box; 13 | } 14 | 15 | body { 16 | color: #555; 17 | font-family: Lato, "Helvetica Neue", Helvetica, sans-serif; 18 | background-color: $background-color; 19 | -webkit-font-smoothing: antialiased; 20 | text-rendering: optimizeLegibility; 21 | } 22 | 23 | h1, h2, h3, h4, h5 { color: #292929; } 24 | h3, h4, h5 { margin: 20px 0 10px; } 25 | h2 { 26 | margin-top: 45px; 27 | margin-bottom: 10px; 28 | border-bottom: 1px solid #dadada; 29 | &:first-of-type { 30 | margin-top: 0; 31 | } 32 | } 33 | 34 | p { margin: 5px 0; } 35 | 36 | ul { 37 | margin: 0; 38 | 39 | ul { 40 | padding-left: 24px; 41 | list-style: none; 42 | } 43 | } 44 | 45 | ol { 46 | margin: 0; 47 | padding: 0; 48 | padding-left: 18px; 49 | li { 50 | padding-left: 8px; 51 | } 52 | } 53 | 54 | a { 55 | text-decoration: none; 56 | } 57 | 58 | a:hover { 59 | text-decoration: none; 60 | color: $dark-orange; 61 | } 62 | 63 | .wrapper { 64 | height: 100%; 65 | } 66 | 67 | .container { 68 | margin-left: $sidebar-width; 69 | @media(max-width: 768px) { 70 | margin: auto; 71 | } 72 | } 73 | 74 | .splitter { 75 | height: 1px; 76 | width: 100%; 77 | margin: 5px 0; 78 | border-bottom: 1px solid #424242; 79 | border-top: 1px solid #212121; 80 | } 81 | 82 | .hidden { 83 | display: none; 84 | } 85 | 86 | .pull-right { 87 | margin-left: auto; 88 | } 89 | 90 | pre.highlight { 91 | padding: 8px 12px; 92 | code { 93 | padding: 0; 94 | color: #ffc; 95 | background-color: transparent; 96 | } 97 | } 98 | code { 99 | padding: 5px; 100 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 101 | font-size: 0.85em; 102 | color: #b87614; 103 | word-wrap: break-word; 104 | background-color: darken($background-color, 3%); 105 | &.hljs { 106 | padding: 1em; 107 | font-size: 14px; 108 | line-height: 1.75; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /jekyll-manager.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/jekyll-manager/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "jekyll-manager" 7 | spec.version = JekyllManager::VERSION 8 | spec.authors = ["Ashwin Maroli"] 9 | spec.email = ["ashmaroli@gmail.com"] 10 | spec.summary = "Jekyll Admin repackaged with some alterations" 11 | spec.description = "An administrative framework for Jekyll sites, Jekyll Manager " \ 12 | "is essentially Jekyll Admin repackaged with some alterations." 13 | spec.homepage = "https://github.com/ashmaroli/jekyll-manager" 14 | spec.license = "MIT" 15 | spec.files = Dir.glob("lib/**/*").concat(%w(LICENSE README.md)) 16 | spec.require_paths = ["lib"] 17 | 18 | spec.required_ruby_version = ">= 2.5.0" 19 | 20 | spec.add_runtime_dependency "jekyll", ">= 3.7", "< 5.0" 21 | spec.add_runtime_dependency "oj", "~> 3.12" 22 | spec.add_runtime_dependency "sinatra", "~> 1.4" 23 | spec.add_runtime_dependency "sinatra-contrib", "~> 1.4" 24 | spec.add_runtime_dependency "webrick", "~> 1.7" 25 | 26 | spec.add_development_dependency "bundler", ">= 1.7" 27 | spec.add_development_dependency "gem-release", "~> 0.7" 28 | spec.add_development_dependency "rake", ">= 12.0" 29 | spec.add_development_dependency "rspec", "~> 3.4" 30 | spec.add_development_dependency "rubocop-jekyll", "~> 0.12.0" 31 | spec.add_development_dependency "sinatra-cross_origin", "~> 0.3" 32 | end 33 | -------------------------------------------------------------------------------- /lib/jekyll-admin/directory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllAdmin 4 | class Directory 5 | extend Forwardable 6 | def_delegator :@path, :basename, :name 7 | def_delegator :@path, :mtime, :modified_time 8 | attr_reader :path, :splat, :content_type, :base 9 | 10 | include Enumerable 11 | include JekyllAdmin::URLable 12 | include JekyllAdmin::APIable 13 | 14 | TYPE = :directory 15 | 16 | # Arguments: 17 | # 18 | # path - full path of the directory which its entries will be listed 19 | # 20 | # base - passes site.source to generate `relative_path` needed for `to_api` 21 | # 22 | # content_type - type of the requested directory entries, this is used to generate 23 | # API endpoint of the directory along with `splat` 24 | # 25 | # splat - the requested directory path relative to content namespace 26 | def initialize(path, base: nil, content_type: nil, splat: nil) 27 | @base = Pathname.new base 28 | @content_type = content_type 29 | @splat = Pathname.new splat 30 | @path = Pathname.new path 31 | end 32 | 33 | def to_liquid 34 | { 35 | :name => name, 36 | :modified_time => modified_time, 37 | :path => relative_path, 38 | :type => TYPE, 39 | } 40 | end 41 | 42 | def relative_path 43 | if content_type == "drafts" 44 | path.relative_path_from(base).to_s.sub("_drafts/", "") 45 | else 46 | path.relative_path_from(base).to_s 47 | end 48 | end 49 | 50 | def resource_path 51 | types = %w(pages data drafts static_files templates theme) 52 | if types.include?(content_type) 53 | "/#{content_type}/#{splat}/#{name}" 54 | else 55 | "/collections/#{content_type}/entries/#{splat}/#{name}" 56 | end 57 | end 58 | alias_method :url, :resource_path 59 | 60 | def http_url 61 | nil 62 | end 63 | 64 | def directories 65 | path.entries.map do |entry| 66 | next if [".", ".."].include? entry.to_s 67 | next unless path.join(entry).directory? 68 | 69 | self.class.new( 70 | path.join(entry), 71 | :base => base, :content_type => content_type, :splat => splat 72 | ) 73 | end.compact! 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/jekyll-admin/file_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllAdmin 4 | module FileHelper 5 | # The file the user requested in the URL 6 | def requested_file 7 | find_by_path(path) 8 | end 9 | 10 | # The file ultimately written to disk 11 | # This may be the requested file, or in the case of a rename will be read 12 | # from the new path that was actually written to disk 13 | def written_file 14 | find_by_path(write_path) 15 | end 16 | 17 | # Write a file to disk with the given content 18 | def write_file(path, content) 19 | Jekyll.logger.debug "WRITING:", path 20 | path = sanitized_path(path) 21 | FileUtils.mkdir_p File.dirname(path) 22 | File.open(path, "wb") do |file| 23 | file.write(content) 24 | end 25 | site.process 26 | end 27 | 28 | # Delete the file at the given path 29 | def delete_file(path) 30 | Jekyll.logger.debug "DELETING:", path 31 | FileUtils.rm_f sanitized_path(path) 32 | site.process 33 | end 34 | 35 | private 36 | 37 | def ensure_requested_file 38 | ensure_file(requested_file) 39 | end 40 | 41 | def ensure_written_file 42 | ensure_file(written_file) 43 | end 44 | 45 | def find_by_path(path) 46 | files = case namespace 47 | when "collections" 48 | collection.docs 49 | when "data" 50 | DataFile.all 51 | when "drafts" 52 | drafts 53 | when "pages", "static_files" 54 | site.public_send(namespace.to_sym) 55 | else 56 | [] 57 | end 58 | files.find { |f| sanitized_path(f.path) == path } 59 | end 60 | 61 | def ensure_file(file) 62 | render_404 if file.nil? 63 | end 64 | 65 | def ensure_directory 66 | render_404 unless Dir.exist?(directory_path) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/jekyll-admin/server/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllAdmin 4 | class Server < Sinatra::Base 5 | namespace "/configuration" do 6 | get do 7 | json( 8 | :content => parsed_configuration, 9 | :raw_content => raw_configuration 10 | ) 11 | end 12 | 13 | put do 14 | write_file(configuration_path, configuration_body) 15 | json request_payload 16 | end 17 | 18 | private 19 | 20 | def overrides 21 | { 22 | "source" => sanitized_path("/"), 23 | } 24 | end 25 | 26 | # Computed configuration, with updates and defaults 27 | def configuration 28 | @configuration ||= Jekyll.configuration(overrides) 29 | end 30 | 31 | # Configuration data, as read by Jekyll 32 | def parsed_configuration 33 | configuration.read_config_file(configuration_path) 34 | end 35 | 36 | # Raw configuration content, as it sits on disk 37 | def raw_configuration 38 | File.read( 39 | configuration_path, 40 | **Jekyll::Utils.merged_file_read_opts(site, {}) 41 | ) 42 | end 43 | 44 | # Returns the path to the *first* config file from the list passed to terminal 45 | # switch +--config+ or the first of default config files to be discovered at the 46 | # site's source directory. 47 | def configuration_path 48 | if site.config["config"] 49 | sanitized_path site.config["config"].first 50 | else 51 | sanitized_path configuration.config_files(overrides).first 52 | end 53 | end 54 | 55 | # The user's uploaded configuration for updates 56 | # Instead of extracting `raw_content` directly from the `request_payload`, 57 | # assign the data to a new variable and then extract the `raw_content` 58 | # from it to circumvent CORS violation in `development` mode. 59 | def configuration_body 60 | payload = request_payload 61 | payload["raw_content"] 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/jekyll-admin/static_server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllAdmin 4 | class StaticServer < Sinatra::Base 5 | set :public_dir, File.expand_path("./public", File.dirname(__FILE__)) 6 | 7 | MUST_BUILD_MESSAGE = "Front end not yet built. Run `script/build` to build." 8 | 9 | # Allow `/admin` and `/admin/`, and `/admin/*` to serve `/public/dist/index.html` 10 | get "/*" do 11 | send_file index_path 12 | end 13 | 14 | # Provide a descriptive error message in dev. if frontend is not build 15 | not_found do 16 | status 404 17 | MUST_BUILD_MESSAGE if settings.development? || settings.test? 18 | end 19 | 20 | private 21 | 22 | def index_path 23 | @index_path ||= File.join(settings.public_folder, "index.html") 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/jekyll-manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Default Sinatra to "production" mode (surpress errors) unless 4 | # otherwise specified by the `RACK_ENV` environmental variable. 5 | # Must be done prior to requiring Sinatra, or we'll get a LoadError 6 | # as it looks for sinatra/cross-origin, which is development only 7 | ENV["RACK_ENV"] = "production" if ENV["RACK_ENV"].to_s.empty? 8 | 9 | require "oj" # Optimized JSON. https://github.com/ohler55/oj 10 | require "jekyll" 11 | require "base64" 12 | require "webrick" 13 | require "sinatra" 14 | require "fileutils" 15 | require "sinatra/base" 16 | require "sinatra/json" 17 | require "addressable/uri" 18 | require "sinatra/reloader" 19 | require "sinatra/namespace" 20 | 21 | module JekyllManager 22 | autoload :VERSION, "jekyll-manager/version" 23 | end 24 | 25 | module JekyllAdmin 26 | autoload :APIable, "jekyll-admin/apiable" 27 | autoload :DataFile, "jekyll-admin/data_file" 28 | autoload :Directory, "jekyll-admin/directory" 29 | autoload :FileHelper, "jekyll-admin/file_helper" 30 | autoload :PathHelper, "jekyll-admin/path_helper" 31 | autoload :Server, "jekyll-admin/server" 32 | autoload :StaticServer, "jekyll-admin/static_server" 33 | autoload :URLable, "jekyll-admin/urlable" 34 | 35 | def self.site 36 | @site ||= begin 37 | site = Jekyll.sites.first 38 | site.future = true 39 | site 40 | end 41 | end 42 | end 43 | 44 | # Monkey Patches 45 | require_relative "./jekyll/commands/serve" 46 | require_relative "./jekyll/commands/build" 47 | 48 | [Jekyll::Page, Jekyll::Document, Jekyll::StaticFile, Jekyll::Collection].each do |klass| 49 | klass.include JekyllAdmin::APIable 50 | klass.include JekyllAdmin::URLable 51 | end 52 | -------------------------------------------------------------------------------- /lib/jekyll-manager/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module JekyllManager 4 | VERSION = "0.1.1" 5 | end 6 | -------------------------------------------------------------------------------- /lib/jekyll/commands/build.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class Build < Command 6 | class << self 7 | alias_method :original_build, :build 8 | 9 | def build(site, options) 10 | options["watch"] = false 11 | original_build(site, options) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/jekyll/commands/serve.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class Serve < Command 6 | class << self 7 | def start_up_webrick(opts, destination) 8 | server = WEBrick::HTTPServer.new(webrick_opts(opts)).tap { |o| o.unmount("") } 9 | server.mount(opts["baseurl"], Servlet, destination, file_handler_opts) 10 | 11 | jekyll_admin_monkey_patch(server) 12 | 13 | Jekyll.logger.info "Server address:", server_address(server, opts) 14 | launch_browser server, opts if opts["open_url"] 15 | boot_or_detach server, opts 16 | end 17 | 18 | def jekyll_admin_monkey_patch(server) 19 | server.mount "/admin", Rack::Handler::WEBrick, JekyllAdmin::StaticServer 20 | server.mount "/_api", Rack::Handler::WEBrick, JekyllAdmin::Server 21 | Jekyll.logger.warn "Auto-regeneration:", "disabled by Jekyll Manager." 22 | Jekyll.logger.warn "", "The site will regenerate only via the Admin interface." 23 | Jekyll.logger.info "Jekyll Manager mode:", ENV["RACK_ENV"] || "production" 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/screenshot.png -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | script/branding 6 | bundle install 7 | npm install 8 | -------------------------------------------------------------------------------- /script/branding: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo " _ _ _ _ __ __ " 4 | echo " | | | | | | | | \/ | " 5 | echo " | | ___| | ___ _| | | | \ / | __ _ _ __ __ _ __ _ ___ _ __ " 6 | echo " _ | |/ _ \ |/ / | | | | | | |\/| |/ _' | '_ \ / _' |/ _' |/ _ \ '__|" 7 | echo "| |__| | __/ <| |_| | | | | | | | (_| | | | | (_| | (_| | __/ | " 8 | echo " \____/ \___|_|\_\__, |_|_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_| " 9 | echo " __/ | __/ | " 10 | echo " |___/ |___/ " 11 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Building frontend server..." 6 | npm run clean-dist 7 | npm run build 8 | 9 | # Test the size of production bundle 10 | # Refer `bundlesize.config.json` for list of tested paths. 11 | npm run test:size 12 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | script/branding 6 | 7 | if [ -z "$TEST_SUITE" ] 8 | then 9 | script/cibuild-node 10 | script/cibuild-ruby 11 | elif [ -x "script/cibuild-$TEST_SUITE" ] 12 | then 13 | script/cibuild-$TEST_SUITE 14 | else 15 | echo "Unknown test suite." 16 | exit 1 17 | fi 18 | -------------------------------------------------------------------------------- /script/cibuild-node: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Running Node tests..." 6 | 7 | # We want to run `script/build` on CI to ensure things actually build, but 8 | # we don't want to have to run `script/build` locally every time we run tests 9 | # 10 | # Note: `script/build` runs `npm test` as part of the build process 11 | if [ -n "$CI" ]; then 12 | script/build 13 | else 14 | npm run test:CI 15 | fi 16 | -------------------------------------------------------------------------------- /script/cibuild-ruby: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | echo "Running Ruby tests..." 6 | RACK_ENV=test JEKYLL_LOG_LEVEL=warn bundle exec rspec 7 | 8 | # Appveyor doesn't seem to like Rubocop for some reason 9 | if [ -z "$APPVEYOR" ]; then 10 | script/fmt 11 | fi 12 | -------------------------------------------------------------------------------- /script/docs-server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd docs || exit 6 | bundle exec jekyll serve 7 | -------------------------------------------------------------------------------- /script/fmt: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | bundle exec rubocop -S -D 6 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | script/build 6 | bundle exec gem release --tag 7 | -------------------------------------------------------------------------------- /script/server-frontend: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | npm start 4 | -------------------------------------------------------------------------------- /script/test-server: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | script/branding 6 | 7 | cd ./spec/fixtures/site || exit 8 | RACK_ENV=development bundle exec jekyll serve --verbose --watch 9 | -------------------------------------------------------------------------------- /spec/fixtures/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jekyll Manager 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /spec/fixtures/site/_config.yml: -------------------------------------------------------------------------------- 1 | title: Your awesome title 2 | 3 | # Load the custom admin interface. 4 | plugins: 5 | - jekyll-manager 6 | 7 | # Gem-based theme for this site. 8 | theme: test-theme 9 | 10 | foo: bar 11 | url: "" 12 | 13 | # show draft posts 14 | show_drafts: true 15 | 16 | defaults: 17 | - 18 | scope: 19 | path: "" 20 | values: 21 | all: true 22 | - 23 | scope: 24 | path: "" 25 | type: pages 26 | values: 27 | page_only: true 28 | - 29 | scope: 30 | path: "" 31 | type: puppies 32 | values: 33 | breed: "" 34 | - 35 | scope: 36 | path: test 37 | type: posts 38 | values: 39 | post_test_only: true 40 | 41 | # Dummy Collection. 42 | collections: 43 | puppies: 44 | foo: bar 45 | output: false 46 | -------------------------------------------------------------------------------- /spec/fixtures/site/_config_test.yml: -------------------------------------------------------------------------------- 1 | title: Custom Test Site 2 | plugins: ["jekyll-manager"] 3 | -------------------------------------------------------------------------------- /spec/fixtures/site/_data/data_file.yml: -------------------------------------------------------------------------------- 1 | foo: bar 2 | -------------------------------------------------------------------------------- /spec/fixtures/site/_data/members.csv: -------------------------------------------------------------------------------- 1 | name,age,sex,location 2 | John Doe,24,Male,New York 3 | Jane Doe,21,Female,New York 4 | -------------------------------------------------------------------------------- /spec/fixtures/site/_data/members.tsv: -------------------------------------------------------------------------------- 1 | name age sex location 2 | John 24 Male New York 3 | Jane 21 Female New York 4 | -------------------------------------------------------------------------------- /spec/fixtures/site/_data/movies/actors.yml: -------------------------------------------------------------------------------- 1 | foo: bar 2 | -------------------------------------------------------------------------------- /spec/fixtures/site/_data/movies/genres/fiction.yml: -------------------------------------------------------------------------------- 1 | foo: bar 2 | -------------------------------------------------------------------------------- /spec/fixtures/site/_data/plain_text.txt: -------------------------------------------------------------------------------- 1 | A dummy file to test if regular text files are accessible via the Admin 2 | interface. 3 | 4 | name | age | sex | location 5 | "John Doe" | 24 | Male | "New York" 6 | "Jane Doe" | 21 | Female | "New York" 7 | -------------------------------------------------------------------------------- /spec/fixtures/site/_data/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": { 3 | "title": "Your awesome title", 4 | "gems": [ 5 | "jekyll-admin" 6 | ], 7 | "exclude": [ 8 | "Gemfile", 9 | "Gemfile.lock", 10 | "README.md" 11 | ], 12 | "foo": "bar", 13 | "defaults": [ 14 | { 15 | "scope": { 16 | "path": "" 17 | }, 18 | "values": { 19 | "some_front_matter": "default" 20 | } 21 | } 22 | ], 23 | "collections": { 24 | "puppies": { 25 | "foo": "bar", 26 | "output": false 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /spec/fixtures/site/_drafts/draft-dir/WIP/yet-another-draft-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | --- 4 | 5 | # Yet Another Draft Post 6 | -------------------------------------------------------------------------------- /spec/fixtures/site/_drafts/draft-dir/another-draft-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | --- 4 | 5 | # Another Draft Post 6 | -------------------------------------------------------------------------------- /spec/fixtures/site/_drafts/draft-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | --- 4 | 5 | # Draft Post 6 | -------------------------------------------------------------------------------- /spec/fixtures/site/_drafts/random/notes.txt: -------------------------------------------------------------------------------- 1 | not a draft 2 | -------------------------------------------------------------------------------- /spec/fixtures/site/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ page.title | default: site.title }} 6 | 7 | 8 |
9 |
10 | {{ content }} 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/site/_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/site/_posts/2016-01-01-test-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | tags: 4 | - test 5 | - foo 6 | --- 7 | 8 | # Test Post 9 | -------------------------------------------------------------------------------- /spec/fixtures/site/_posts/2016-02-01-test-post-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | tags: 4 | - test 5 | - safari 6 | --- 7 | 8 | # Test Post2 9 | -------------------------------------------------------------------------------- /spec/fixtures/site/_posts/2016-03-01-test-post-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | tags: 4 | - highlight 5 | --- 6 | 7 | # Test Post with highlighting 8 | 9 | ```ruby 10 | # Gemfile 11 | 12 | group :jekyll_plugins do 13 | gem "jekyll-admin" 14 | end 15 | ``` 16 | -------------------------------------------------------------------------------- /spec/fixtures/site/_posts/more posts/2016-04-01-post-within-subdirectory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Post Within Subdirectory 3 | foo: bar 4 | --- 5 | 6 | # Test Post within `_posts/more posts` 7 | -------------------------------------------------------------------------------- /spec/fixtures/site/_posts/more posts/some more posts/2016-05-01-a-test-post-within-subdirectory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: A Test Post Within Subdirectory 3 | foo: bar 4 | --- 5 | 6 | # Test Post within `_posts/more posts/some more posts` 7 | -------------------------------------------------------------------------------- /spec/fixtures/site/_posts/more posts/some more posts/2016-05-02-another-test-post-within-subdirectory.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Another Test Post Within Subdirectory 3 | foo: bar 4 | --- 5 | 6 | # Another Test Post within `_posts/more posts/some more posts` 7 | -------------------------------------------------------------------------------- /spec/fixtures/site/_posts/test/2016-01-02-test2.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar2 3 | --- 4 | 5 | test -------------------------------------------------------------------------------- /spec/fixtures/site/_puppies/rover.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rover 3 | breed: Golden Retriever 4 | --- 5 | -------------------------------------------------------------------------------- /spec/fixtures/site/_sass/main.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #21abcd; 2 | 3 | @import "test-dir/base"; 4 | -------------------------------------------------------------------------------- /spec/fixtures/site/_sass/test-dir/_base.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | color: $primary-color; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/site/assets/app.coffee: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | alert('hello world') 5 | -------------------------------------------------------------------------------- /spec/fixtures/site/assets/images/icon-github.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/fixtures/site/assets/images/jekyll-logo-light-solid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/spec/fixtures/site/assets/images/jekyll-logo-light-solid.png -------------------------------------------------------------------------------- /spec/fixtures/site/assets/scripts/script.js: -------------------------------------------------------------------------------- 1 | var msg = "Hello World"; 2 | alert(msg); 3 | -------------------------------------------------------------------------------- /spec/fixtures/site/assets/static-file.txt: -------------------------------------------------------------------------------- 1 | TEST 2 | -------------------------------------------------------------------------------- /spec/fixtures/site/assets/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | body { padding: 20; } 5 | -------------------------------------------------------------------------------- /spec/fixtures/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | You're probably looking for /admin. 4 | 5 | 6 | -------------------------------------------------------------------------------- /spec/fixtures/site/page-dir/page1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: test 3 | foo: bar 4 | --- 5 | 6 | # Test Page 1 7 | -------------------------------------------------------------------------------- /spec/fixtures/site/page-dir/test/page2.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | --- 4 | 5 | # Test Page 2 6 | -------------------------------------------------------------------------------- /spec/fixtures/site/page.md: -------------------------------------------------------------------------------- 1 | --- 2 | foo: bar 3 | --- 4 | 5 | # Test Page 6 | -------------------------------------------------------------------------------- /spec/fixtures/site/static-file.txt: -------------------------------------------------------------------------------- 1 | TEST 2 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jekyll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/_includes/include.html: -------------------------------------------------------------------------------- 1 | include.html from test-theme 2 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ page.title | default: site.title }} 6 | 7 | 8 |
9 |
10 | {{ content }} 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/_layouts/page.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 | 9 |
10 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/_sass/test-theme-black.scss: -------------------------------------------------------------------------------- 1 | .sample { 2 | color: black; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/_sass/test-theme-red.scss: -------------------------------------------------------------------------------- 1 | .sample { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/assets/application.coffee: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | alert "From your theme." 5 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/assets/images/icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/spec/fixtures/test-theme/assets/images/icon-dark.png -------------------------------------------------------------------------------- /spec/fixtures/test-theme/assets/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "test-theme-{{ site.theme-color | default: "red" }}"; 5 | -------------------------------------------------------------------------------- /spec/fixtures/test-theme/test-theme.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "test-theme" 5 | s.version = "0.1.0" 6 | s.license = "MIT" 7 | s.summary = "This is a theme used to test Jekyll Manager." 8 | s.description = "A gem-based Jekyll theme originally used at jekyll/jekyll " \ 9 | "repository for testing purposes has been slightly modified " \ 10 | "for testing this fork of jekyll/jekyll-admin" 11 | s.authors = ["Jekyll"] 12 | s.files = Dir.glob("#{__dir__}/**/*") 13 | s.homepage = "https://github.com/ashmaroli/jekyll-manager" 14 | end 15 | -------------------------------------------------------------------------------- /spec/jekyll-admin/apiable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe JekyllAdmin::APIable do 4 | [:page, :post].each do |type| 5 | context type do 6 | subject do 7 | documents = Jekyll.sites.first.send("#{type}s".to_sym) 8 | if type == :page 9 | documents.find(&:html?) 10 | else 11 | documents.docs.first 12 | end 13 | end 14 | 15 | [false, true].each do |with_content| 16 | context "#{with_content ? "with" : "without"} content" do 17 | let(:as_api) { subject.to_api(:include_content => with_content) } 18 | let(:content) { as_api["content"] } 19 | let(:raw_content) { as_api["raw_content"] } 20 | let(:front_matter) { as_api["front_matter"] } 21 | 22 | it "is responds to to_api" do 23 | expect(subject).to respond_to(:to_api) 24 | end 25 | 26 | if with_content 27 | it "includes the raw_content" do 28 | expect(raw_content).to eql("# Test #{type.to_s.capitalize}\n") 29 | end 30 | 31 | it "includes the rendered content" do 32 | expected = "

Test #{type.capitalize}

\n" 33 | expect(content).to eql(expected) 34 | end 35 | 36 | it "includes the raw front matter" do 37 | expect(front_matter).to have_key("foo") 38 | expect(front_matter["foo"]).to eql("bar") 39 | end 40 | 41 | it "doesn't include front matter defaults in the raw front matter" do 42 | expect(front_matter).to_not have_key("some_front_matter") 43 | end 44 | else 45 | 46 | it "doesn't include the raw content" do 47 | expect(as_api).to_not have_key("raw_content") 48 | end 49 | 50 | it "doesn't include the rendered content" do 51 | expect(as_api).to_not have_key("content") 52 | expect(as_api).to_not have_key("output") 53 | end 54 | end 55 | 56 | it "includes front matter defaults as top-level keys" do 57 | expect(as_api).to have_key("all") 58 | expect(as_api["all"]).to eql(true) 59 | end 60 | 61 | it "includes front matter as top-level keys" do 62 | expect(as_api["foo"]).to eql("bar") 63 | end 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/jekyll-admin/custom_integration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "custom integration" do 4 | let(:source) { fixture_path("site") } 5 | let(:dest) { File.join(source, "_site") } 6 | let(:config) { File.join(source, "_config_test.yml") } 7 | 8 | let(:args) do 9 | [ 10 | "--detach", 11 | "--watch", 12 | "--source", source, 13 | "--destination", dest, 14 | "--config", config, 15 | ] 16 | end 17 | 18 | let(:start_command) { %w(bundle exec jekyll serve).concat(args) } 19 | let(:stop_command) { ["pkill", "-f", "jekyll"] } 20 | let(:server) { "http://localhost:4000" } 21 | let(:path) { "/" } 22 | let(:uri) { URI.join(server, path) } 23 | let(:response) { Net::HTTP.get_response(uri) } 24 | 25 | before do 26 | Open3.popen3(*start_command) 27 | sleep 3 28 | end 29 | 30 | after { Open3.capture2e(*stop_command) } unless ENV["GITHUB_ACTION"] 31 | 32 | context "Jekyll site" do 33 | let(:path) { "/" } 34 | 35 | it "serves the Jekyll site", :skip => Gem.win_platform? do 36 | expect(response.code).to eql("200") 37 | expect(response.body).to match("You're probably looking for") 38 | end 39 | end 40 | 41 | context "Admin site" do 42 | let(:path) { "/admin" } 43 | 44 | it "serves the Jekyll site", :skip => Gem.win_platform? do 45 | with_index_stubbed do 46 | expect(response.code).to eql("200") 47 | expect(response.body).to match("Jekyll Manager") 48 | end 49 | end 50 | end 51 | 52 | context "API" do 53 | let(:path) { "/_api/configuration" } 54 | 55 | it "serves the Jekyll site", :skip => Gem.win_platform? do 56 | expect(response.code).to eql("200") 57 | expect(response.body).to match("Custom Test Site") 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/jekyll-admin/data_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe JekyllAdmin::DataFile do 4 | let(:data_dir) { "_data" } 5 | let(:relative_path) { "data_file.yml" } 6 | let(:absolute_path) { in_source_dir(relative_path) } 7 | let(:id) { "data_file.yml" } 8 | 9 | subject { described_class.new(id) } 10 | 11 | it "exposes the relative path" do 12 | expect(subject.relative_path).to eql(relative_path) 13 | end 14 | 15 | it "exposes the raw content" do 16 | expect(subject.raw_content).to eql("foo: bar\n") 17 | end 18 | 19 | it "exposes the parsed content" do 20 | expect(subject.content["foo"]).to eql("bar") 21 | end 22 | 23 | it "exposes the extension" do 24 | expect(subject.extension).to eql(".yml") 25 | end 26 | 27 | it "exposes the slug" do 28 | expect(subject.slug).to eql("data_file") 29 | end 30 | 31 | it "exposes the title" do 32 | expect(subject.title).to eql("Data File") 33 | end 34 | 35 | it "returns the hash" do 36 | expect(subject.to_api).to eql( 37 | "path" => "/_data/data_file.yml", 38 | "relative_path" => "data_file.yml", 39 | "slug" => "data_file", 40 | "ext" => ".yml", 41 | "title" => "Data File", 42 | "api_url" => "http://localhost:4000/_api/data/data_file.yml", 43 | "http_url" => nil 44 | ) 45 | end 46 | 47 | it "returns the hash with content" do 48 | expect(subject.to_api(:include_content => true)).to eql( 49 | "path" => "/_data/data_file.yml", 50 | "relative_path" => "data_file.yml", 51 | "slug" => "data_file", 52 | "ext" => ".yml", 53 | "title" => "Data File", 54 | "raw_content" => "foo: bar\n", 55 | "content" => { 56 | "foo" => "bar", 57 | }, 58 | "api_url" => "http://localhost:4000/_api/data/data_file.yml", 59 | "http_url" => nil 60 | ) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/jekyll-admin/integration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "integration" do 4 | let(:source) { fixture_path("site") } 5 | let(:dest) { File.join(source, "_site") } 6 | let(:args) { ["--detach", "--watch", "--source", source, "--destination", dest] } 7 | let(:start_command) { %w(bundle exec jekyll serve).concat(args) } 8 | let(:stop_command) { ["pkill", "-f", "jekyll"] } 9 | let(:server) { "http://localhost:4000" } 10 | let(:path) { "/" } 11 | let(:uri) { URI.join(server, path) } 12 | let(:response) { Net::HTTP.get_response(uri) } 13 | 14 | before do 15 | Open3.popen3(*start_command) 16 | sleep 3 17 | end 18 | 19 | after { Open3.capture2e(*stop_command) } unless ENV["GITHUB_ACTION"] 20 | 21 | context "Jekyll site" do 22 | let(:path) { "/" } 23 | 24 | it "serves the Jekyll site", :skip => Gem.win_platform? do 25 | expect(response.code).to eql("200") 26 | expect(response.body).to match("You're probably looking for") 27 | end 28 | end 29 | 30 | context "Admin site" do 31 | let(:path) { "/admin" } 32 | 33 | it "serves the Jekyll site", :skip => Gem.win_platform? do 34 | with_index_stubbed do 35 | expect(response.code).to eql("200") 36 | expect(response.body).to match("Jekyll Manager") 37 | end 38 | end 39 | end 40 | 41 | context "API" do 42 | let(:path) { "/_api" } 43 | 44 | it "serves the Jekyll site", :skip => Gem.win_platform? do 45 | expect(response.code).to eql("200") 46 | expect(response.body).to match("collections_api") 47 | end 48 | end 49 | 50 | context "watching" do 51 | it "Jekyll isn't watching", :skip => Gem.win_platform? do 52 | File.open(File.join(source, "page.md"), "a") do |f| 53 | f.puts "peek-a-boo" 54 | end 55 | content = File.read(File.join(source, "page.md")) 56 | output = File.read(File.join(dest, "page.html")) 57 | 58 | expect(content).to include("peek-a-boo") 59 | expect(output).not_to include("peek-a-boo") 60 | 61 | File.open(File.join(source, "page.md"), "w+") do |f| 62 | f.write "---\nfoo: bar\n---\n\n# Test Page\n" 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/jekyll-admin/server/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "configuration" do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | JekyllAdmin::Server 8 | end 9 | 10 | it "returns the configuration" do 11 | get "/configuration" 12 | expect(last_response).to be_ok 13 | expect(last_response_parsed["content"]["foo"]).to eql("bar") 14 | expect(last_response_parsed.key?("source")).to eql(false) 15 | end 16 | 17 | it "updates the configuration" do 18 | config_path = File.expand_path "_config.yml", fixture_path("site") 19 | config_body = File.read(config_path) 20 | config_before = SafeYAML.load(config_body) 21 | config = config_before.dup 22 | 23 | config["foo"] = "bar2" 24 | begin 25 | put "/configuration", config.to_json 26 | 27 | expect(last_response).to be_ok 28 | expect(last_response_parsed["foo"]).to eql("bar2") 29 | expect(last_response_parsed.key?("source")).to eql(false) 30 | ensure 31 | File.write(config_path, config_body) 32 | end 33 | end 34 | 35 | it "doesn't inject the default collections" do 36 | config_path = File.expand_path "_config.yml", fixture_path("site") 37 | config_body = File.read(config_path) 38 | config_before = SafeYAML.load(config_body) 39 | 40 | config = config_before.dup 41 | config["collections"]["test"] = { "foo" => "bar" } 42 | 43 | begin 44 | put "/configuration", config.to_json 45 | collections = last_response_parsed["collections"] 46 | expect(collections["test"]["foo"]).to eql("bar") 47 | expect(collections.key?("posts")).to eql(false) 48 | ensure 49 | File.write(config_path, config_body) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/jekyll-admin/server/dashboard_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "dashboard" do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | JekyllAdmin::Server 8 | end 9 | 10 | it "returns the modified site payload" do 11 | get "/dashboard" 12 | expect(last_response).to be_ok 13 | expect(last_response_parsed["site"]["posts"]).to eql( 14 | [ 15 | "2016-01-01-test-post.md", 16 | "2016-02-01-test-post-2.md", 17 | "2016-03-01-test-post-3.md", 18 | "more posts/2016-04-01-post-within-subdirectory.md", 19 | "more posts/some more posts/2016-05-01-a-test-post-within-subdirectory.md", 20 | "more posts/some more posts/2016-05-02-another-test-post-within-subdirectory.md", 21 | "test/2016-01-02-test2.md", 22 | ].sort 23 | ) 24 | expect(last_response_parsed["site"]["content_pages"]).to eql( 25 | [ 26 | "page.md", 27 | "page-dir/page1.md", 28 | "page-dir/test/page2.md", 29 | ].sort 30 | ) 31 | expect(last_response_parsed["site"]["data_files"]).to eql( 32 | [ 33 | "template-config.yaml", 34 | "data_file.yml", 35 | "movies/actors.yml", 36 | "movies/genres/fiction.yml", 37 | "settings.json", 38 | "members.csv", 39 | ].sort 40 | ) 41 | expect(last_response_parsed["site"]["static_files"]).to eql( 42 | [ 43 | "/assets/images/icon-github.svg", 44 | "/assets/images/jekyll-logo-light-solid.png", 45 | "/assets/scripts/script.js", 46 | "/assets/static-file.txt", 47 | "/index.html", 48 | "/static-file.txt", 49 | "/assets/images/icon-dark.png", 50 | ].sort 51 | ) 52 | expect(last_response_parsed["site"]["drafts"]).to eql( 53 | [ 54 | "draft-dir/WIP/yet-another-draft-post.md", 55 | "draft-dir/another-draft-post.md", 56 | "draft-post.md", 57 | ].sort 58 | ) 59 | expect(last_response_parsed["site"]["collections"]).to eql(%w(posts puppies)) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/jekyll-admin/server_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe JekyllAdmin::Server do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | JekyllAdmin::Server 8 | end 9 | 10 | it "returns the page index" do 11 | get "/pages" 12 | expect(last_response).to be_ok 13 | entries = last_response_parsed 14 | first_page = entries.reject do |entry| 15 | entry.key? "type" 16 | end.first 17 | expect(first_page["path"]).to eq("page.md") 18 | end 19 | 20 | it "returns an individual page" do 21 | get "/pages/page.md" 22 | expect(last_response).to be_ok 23 | expect(last_response_parsed["foo"]).to eq("bar") 24 | end 25 | 26 | it "responds to CORS preflight checks" do 27 | options "/", {}, "HTTP_ORIGIN" => "http://localhost:3000" 28 | expect(last_response.status).to eql(204) 29 | 30 | expected = { 31 | "Access-Control-Allow-Origin" => "http://localhost:3000", 32 | "Access-Control-Allow-Methods" => "DELETE, GET, OPTIONS, POST, PUT", 33 | } 34 | 35 | expected.each do |key, value| 36 | expect(last_response.headers[key]).to eql(value) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/jekyll-admin/static_server_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe JekyllAdmin::StaticServer do 4 | include Rack::Test::Methods 5 | 6 | def app 7 | JekyllAdmin::StaticServer 8 | end 9 | 10 | def expected_index 11 | expected = File.read(index_path) 12 | expected.gsub!("\n", "\r\n") if Gem.win_platform? 13 | expected 14 | end 15 | 16 | it "returns the index" do 17 | with_index_stubbed do 18 | get "/" 19 | expect(last_response).to be_ok 20 | expect(last_response.body).to eql(expected_index) 21 | end 22 | end 23 | 24 | it "returns the index for non-existent paths" do 25 | with_index_stubbed do 26 | get "/collections" 27 | expect(last_response).to be_ok 28 | expect(last_response.body).to eql(expected_index) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/jekyll_admin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe JekyllAdmin do 4 | include Rack::Test::Methods 5 | 6 | it "returns the site" do 7 | expect(described_class.site.class).to eql(Jekyll::Site) 8 | expect(described_class.site.source).to eql(fixture_path("site")) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["RACK_ENV"] = "test" 4 | 5 | require "rspec" 6 | require "jekyll-manager" 7 | require "rack/test" 8 | require "fileutils" 9 | require "open3" 10 | require "net/http" 11 | 12 | RSpec.configure do |conf| 13 | conf.include Rack::Test::Methods 14 | end 15 | 16 | def fixture_path(fixture) 17 | File.expand_path "./fixtures/#{fixture}", File.dirname(__FILE__) 18 | end 19 | 20 | def dist_path 21 | File.expand_path "../lib/jekyll-admin/public/", File.dirname(__FILE__) 22 | end 23 | 24 | def index_path 25 | File.expand_path "index.html", dist_path 26 | end 27 | 28 | def stub_index 29 | FileUtils.mkdir_p(dist_path) 30 | FileUtils.cp fixture_path("index.html"), index_path 31 | end 32 | 33 | def with_index_stubbed 34 | return yield if File.exist?(index_path) 35 | 36 | stub_index 37 | yield 38 | FileUtils.rm_f(index_path) 39 | end 40 | 41 | def in_source_dir(questionable_path) 42 | Jekyll.sanitized_path JekyllAdmin.site.source, questionable_path 43 | end 44 | 45 | def last_response_parsed 46 | JSON.parse(last_response.body) 47 | end 48 | 49 | # Deletes one or more files, if they exist within the site, and rebuilds it 50 | def delete_file(*paths) 51 | paths.each do |path| 52 | path = in_source_dir(path) 53 | File.delete(path) if File.exist?(path) 54 | end 55 | JekyllAdmin.site.process 56 | end 57 | 58 | # Writes a file to path 59 | # 60 | # path - the path to the file, relative to the fixture site source 61 | # content - optional, content to write, defaulting to YAML front matter + markdown 62 | # 63 | # Rebuilds the site and returns the full path to the file 64 | def write_file(path, content = "---\n---\n\n# test") 65 | path = in_source_dir(path) 66 | delete_file path 67 | FileUtils.mkdir_p File.dirname(path) 68 | File.write path, content 69 | JekyllAdmin.site.process 70 | path 71 | end 72 | 73 | RSpec::Matchers.define :be_an_existing_file do 74 | match do |path| 75 | path = in_source_dir(path) 76 | File.exist?(path) 77 | end 78 | end 79 | 80 | config = Jekyll.configuration("source" => fixture_path("site")) 81 | site = Jekyll::Site.new(config) 82 | site.process 83 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/lato-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/fonts/lato-bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/lato-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/fonts/lato-regular.ttf -------------------------------------------------------------------------------- /src/assets/images/logo-black-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/images/logo-black-red.png -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashmaroli/jekyll-manager/fd4a9a43a501ff6511795a201ecb42100b82af71/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/components/Breadcrumbs.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router'; 4 | import _ from 'underscore'; 5 | import { toTitleCase } from '../utils/helpers'; 6 | import { ADMIN_PREFIX } from '../constants'; 7 | 8 | export default class Breadcrumbs extends Component { 9 | 10 | render() { 11 | const { splat, type } = this.props; 12 | // generate links from `splat` 13 | let base; 14 | if (type == 'pages' || type == 'drafts' || type == 'templates' || type == 'theme') { 15 | base = `${ADMIN_PREFIX}/${type}`; 16 | } else if (type == 'data files') { 17 | base = `${ADMIN_PREFIX}/datafiles`; 18 | } else if (type == 'static files') { 19 | base = `${ADMIN_PREFIX}/staticfiles`; 20 | } else { 21 | base = `${ADMIN_PREFIX}/collections/${type}`; 22 | } 23 | let links; 24 | if (splat) { 25 | const paths = splat.split('/'); 26 | links = _.map(paths, (path, i) => { 27 | const before = (i == 0) ? '' : paths.slice(0, i).join('/') + '/'; 28 | return { 29 | href: `${base}/${before}${path}`, 30 | label: path 31 | }; 32 | }); 33 | } 34 | 35 | let nodes = _.map(links, (link, i) => { 36 | if (link.href && !link.label.includes('.')) { 37 | return
  • {link.label}
  • ; 38 | } else { 39 | return
  • {link.label}
  • ; 40 | } 41 | }); 42 | 43 | return ( 44 | 48 | ); 49 | } 50 | } 51 | 52 | Breadcrumbs.propTypes = { 53 | splat: PropTypes.string.isRequired, 54 | type: PropTypes.string.isRequired 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/Collapsible.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { findDOMNode } from 'react-dom'; 4 | import classnames from 'classnames'; 5 | 6 | export default class Collapsible extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | 11 | this.toggleCollapse = this.toggleCollapse.bind(this); 12 | 13 | this.state = { 14 | collapsedPanel: true, 15 | height: 0 16 | }; 17 | } 18 | 19 | componentWillReceiveProps(nextProps) { 20 | if (this.props.height !== nextProps.height) { 21 | const panelHeight = nextProps.height; 22 | this.setState({ height: panelHeight + panelHeight }); 23 | } 24 | } 25 | 26 | toggleCollapse() { 27 | const panelHeight = findDOMNode(this.refs.panel).clientHeight + 2; 28 | if (this.state.collapsedPanel) { 29 | this.setState({ 30 | collapsedPanel: false, 31 | height: panelHeight 32 | }); 33 | } 34 | else { 35 | this.setState({ 36 | collapsedPanel: true, 37 | height: 0 38 | }); 39 | } 40 | } 41 | 42 | render() { 43 | const { label, panel, overflow } = this.props; 44 | const collapsibleClasses = classnames({ 45 | "collapsible-toggle": true, 46 | "collapsed": this.state.collapsedPanel 47 | }); 48 | 49 | const panelClasses = classnames({ 50 | "collapsible-panel": true, 51 | "no-overflow": !overflow 52 | }); 53 | 54 | return ( 55 |
    56 |
    57 | {label} 58 |
    59 |
    60 |
    61 |
    62 | {panel} 63 |
    64 |
    65 |
    66 | ); 67 | } 68 | } 69 | 70 | Collapsible.propTypes = { 71 | panel: PropTypes.object.isRequired, 72 | label: PropTypes.string.isRequired, 73 | height: PropTypes.number, 74 | overflow: PropTypes.bool 75 | }; 76 | -------------------------------------------------------------------------------- /src/components/Dropzone.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import _ from 'underscore'; 4 | import ReactDropzone from 'react-dropzone'; 5 | import FilePreview from './FilePreview'; 6 | import Splitter from './Splitter'; 7 | 8 | export class Dropzone extends Component { 9 | 10 | openDropzone() { 11 | this.refs.dropzone.open(); 12 | } 13 | 14 | render() { 15 | const { files, splat, onDrop, onClickDelete, onClickItem } = this.props; 16 | let node; 17 | if (files.length) { 18 | node = ( 19 |
    20 | { 21 | _.map(files, (file, i) => { 22 | return ( 23 | 29 | ); 30 | }) 31 | } 32 | 33 |
    34 | Drag and drop file(s) here to upload additional items 35 |
    36 |
    37 | ); 38 | } else { 39 | node = ( 40 |
    41 |