├── .nvmrc ├── .markdownlint.yml ├── .github ├── FUNDING.yml ├── workflows │ ├── build.yml │ ├── github-pages-deploy.yml │ └── netlify-deploy.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── docs ├── .vuepress │ ├── public │ │ ├── favicon.ico │ │ ├── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ └── msapplication-icon-144x144.png │ │ └── manifest.json │ ├── styles │ │ ├── palette.styl │ │ └── index.styl │ └── config.js ├── guide │ ├── deploying.md │ ├── getting-started.md │ ├── README.md │ ├── available-methods.md │ ├── how-it-works.md │ ├── custom-pages.md │ └── custom-posts.md └── README.md ├── src ├── styles │ ├── vars.css │ ├── types │ │ ├── quote.css │ │ ├── link.css │ │ ├── chat.css │ │ └── photo.css │ ├── components │ │ ├── tags.css │ │ └── card.css │ ├── base.css │ └── reset.css └── scripts │ ├── styles.js │ ├── templates │ ├── posts │ │ ├── text.js │ │ ├── link.js │ │ ├── audio.js │ │ ├── quote.js │ │ ├── video.js │ │ ├── chat.js │ │ └── photo.js │ └── pages │ │ ├── tagged.js │ │ ├── post.js │ │ └── home.js │ ├── utils.js │ └── tumblr-builder.js ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── .npmignore ├── .editorconfig ├── examples ├── basic-usage │ ├── basic-usage.js │ └── index.html ├── custom-pages │ ├── index.html │ └── custom-pages.js ├── custom-posts │ ├── index.html │ └── custom-posts.js ├── dist │ ├── basic-usage.js │ ├── custom-pages.js │ └── custom-posts.js └── webpack.config.js ├── README.md ├── postcss.config.js ├── LICENSE ├── netlify ├── index.html ├── netlify.js ├── netlify.css └── webpack.config.js ├── CHANGELOG.md ├── webpack.config.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.14.0 2 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | MD013: false # Line length 2 | MD033: false # Inline HTML 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [yoriiis] 4 | -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoriiis/tumblr-builder/HEAD/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /src/styles/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --gray-10: rgba(0, 0, 0, 0.1); 3 | --gray-65: rgba(0, 0, 0, 0.65); 4 | --white: #fff; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/types/quote.css: -------------------------------------------------------------------------------- 1 | @import "@commonVars"; 2 | 3 | .card { 4 | .card-blockquote { 5 | margin-bottom: 20px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Local Netlify folder 4 | .netlify 5 | 6 | docs/site 7 | **/*.map 8 | netlify/dist/ 9 | netlify/assets/ 10 | dist 11 | -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoriiis/tumblr-builder/HEAD/docs/.vuepress/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoriiis/tumblr-builder/HEAD/docs/.vuepress/public/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoriiis/tumblr-builder/HEAD/docs/.vuepress/public/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | // colors 2 | $accentColor = #001935 3 | $textColor = #2c3e50 4 | $borderColor = #eaecef 5 | $codeBgColor = #282c34 6 | $arrowBgColor = #ccc 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | "shd101wyy.markdown-preview-enhanced", 6 | "Tobermory.es6-string-html" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .netlify/ 3 | .vscode 4 | docs/ 5 | examples/ 6 | netlify/ 7 | src/ 8 | .editorconfig 9 | .gitignore 10 | .markdownlint.yml 11 | .nvmrc 12 | postcss.config.js 13 | webpack.config.js 14 | -------------------------------------------------------------------------------- /src/styles/types/link.css: -------------------------------------------------------------------------------- 1 | @import "@commonVars"; 2 | 3 | .card-link { 4 | .card-linkItem { 5 | display: block; 6 | padding: 16px 12px; 7 | border: 1px solid var(--gray-10); 8 | margin-bottom: 20px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = tab 11 | indent_size = 4 12 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/styles/types/chat.css: -------------------------------------------------------------------------------- 1 | @import "@commonVars"; 2 | 3 | .card-chat { 4 | .card-conversations { 5 | margin-bottom: 20px; 6 | 7 | li { 8 | margin-bottom: 10px; 9 | 10 | &:last-child { 11 | margin-bottom: 0; 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/scripts/styles.js: -------------------------------------------------------------------------------- 1 | import '../styles/reset.css' 2 | import '../styles/base.css' 3 | 4 | import '../styles/components/tags.css' 5 | import '../styles/components/card.css' 6 | 7 | import '../styles/types/photo.css' 8 | import '../styles/types/quote.css' 9 | import '../styles/types/chat.css' 10 | import '../styles/types/link.css' 11 | -------------------------------------------------------------------------------- /docs/.vuepress/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TumblrBuilder", 3 | "short_name": "TumblrBuilder", 4 | "icons": [ 5 | { 6 | "src": "icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "index.html", 12 | "display": "standalone", 13 | "background_color": "#fff", 14 | "theme_color": "#001935" 15 | } 16 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | .custom-block.tip { 2 | border-color: $accentColor; 3 | } 4 | 5 | .badge.tip { 6 | background-color: $accentColor !important; 7 | } 8 | 9 | /* 10 | * Quick fix in order to wait the next release 11 | * https://github.com/vuejs/vuepress/issues/2334 12 | */ 13 | .sidebar .dropdown-wrapper .dropdown-title { 14 | pointer-events: auto; 15 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | uses: yoriiis/actions/.github/workflows/test-and-build.yml@main 14 | with: 15 | node-version: 12.14 16 | command-build: | 17 | npm run build 18 | npm run build:examples 19 | -------------------------------------------------------------------------------- /.github/workflows/github-pages-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | github: 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | uses: yoriiis/actions/.github/workflows/github-pages-deploy.yml@main 16 | with: 17 | node-version: 12.14 18 | build-dir: docs/site 19 | command-build: npm run docs:build 20 | -------------------------------------------------------------------------------- /examples/basic-usage/basic-usage.js: -------------------------------------------------------------------------------- 1 | const tumblr = new window.TumblrBuilder({ 2 | element: document.querySelector('#tumblr-app'), 3 | host: 'tmblr-builder.tumblr.com', 4 | apiKey: 'X3Hk9uvqCx5OJJVKm9AZX8uh6wf1OhLPtKK5vCJcmQUyngWabO', 5 | limitData: 250, 6 | cache: true, 7 | cacheMethod: 'sessionStorage', 8 | nearBottom: 350, 9 | elementsPerPage: 2 10 | }) 11 | 12 | // Initialize the Tumblr from the instance 13 | tumblr.init().then(response => { 14 | console.log(response) 15 | }) 16 | -------------------------------------------------------------------------------- /src/styles/components/tags.css: -------------------------------------------------------------------------------- 1 | @import "@commonVars"; 2 | 3 | .tags { 4 | margin-bottom: 20px; 5 | 6 | .card-body { 7 | padding-bottom: 10px; 8 | } 9 | 10 | li { 11 | margin-bottom: 10px; 12 | margin-right: 10px; 13 | display: inline-block; 14 | vertical-align: top; 15 | 16 | &:last-child { 17 | margin-right: 0; 18 | } 19 | } 20 | 21 | a { 22 | color: var(--gray-65); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/basic-usage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Basic usage - TumblrBuilder 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/custom-pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Custom pages - TumblrBuilder 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/custom-posts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Custom posts - TumblrBuilder 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/netlify-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Netlify 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | netlify: 10 | uses: yoriiis/actions/.github/workflows/netlify-deploy.yml@main 11 | with: 12 | node-version: 12.14 13 | build-dir: netlify 14 | command-build: | 15 | npm run build 16 | npm run build:netlify 17 | cp -r dist/ netlify/assets/ 18 | secrets: 19 | NETLIFY_APP_ID: ${{ secrets.NETLIFY_APP_ID }} 20 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 21 | -------------------------------------------------------------------------------- /docs/guide/deploying.md: -------------------------------------------------------------------------------- 1 | # Deploying 2 | 3 | Deploy the app on Tumblr or to an external site. 4 | 5 | ## Tumblr 6 | 7 | Tumblr blog have a customize page to import custom HTML. 8 | 9 | - Copy the HTML content 10 | - Open Tumblr editor `https://www.tumblr.com/customize/` 11 | - Paste the HTML content 12 | - Host the asset files (scripts and styles) 13 | - Navigate to the Tumblr 14 | 15 | ## External site 16 | 17 | All requests use the Tumblr API, so the app can works outside the Tumblr ecosystem and be host on any external site with the usual processes. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TumblrBuilder 2 | 3 | ![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/yoriiis/tumblr-builder/build.yml?branch=master&style=for-the-badge) 4 | 5 | ## Installation 6 | 7 | First, install the library from [npm](https://www.npmjs.com/package/tumblr-builder). 8 | 9 | ```bash 10 | npm install tumblr-builder --save-dev 11 | ``` 12 | 13 | ## Documentation 14 | 15 | Check out the docs at [yoriiis.github.io/tumblr-builder](https://yoriiis.github.io/tumblr-builder). 16 | 17 | ## License 18 | 19 | TumblrBuilder and his documentation are under the [MIT License](http://opensource.org/licenses/MIT). 20 | 21 | Created with ♥ by [@yoriiis](http://github.com/yoriiis). 22 | -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | @import "@commonVars"; 2 | 3 | body { 4 | font-family: "Helvetica Neue", Arial, sans-serif; 5 | } 6 | 7 | .container { 8 | max-width: 500px; 9 | margin: 0 auto; 10 | padding: 10px 10px 0; 11 | } 12 | 13 | .nav { 14 | margin: 20px 0; 15 | } 16 | 17 | .subnav { 18 | margin-bottom: 20px; 19 | } 20 | 21 | .relatedPosts { 22 | &-title { 23 | margin-bottom: 24px; 24 | padding-top: 20px; 25 | } 26 | } 27 | 28 | .posts { 29 | margin-bottom: 20px; 30 | } 31 | 32 | a { 33 | text-decoration: none; 34 | color: #000; 35 | 36 | &:hover { 37 | text-decoration: underline; 38 | } 39 | } 40 | 41 | strong { 42 | font-weight: 500; 43 | } 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🚀 Feature request" 3 | about: Suggest an idea for this project 4 | labels: enhancement 5 | --- 6 | 7 | ## 🚀 Feature Proposal 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /src/styles/types/photo.css: -------------------------------------------------------------------------------- 1 | @import "@commonVars"; 2 | 3 | .card-photo { 4 | .card-photos { 5 | margin-bottom: 20px; 6 | } 7 | 8 | &.photoset { 9 | .card-photos { 10 | display: grid; 11 | grid-template-columns: repeat(2, 1fr); 12 | grid-row-gap: 20px; 13 | grid-column-gap: 20px; 14 | align-items: center; 15 | 16 | li { 17 | display: none; 18 | } 19 | 20 | li:nth-child(3n + 1) { 21 | grid-column: 1/3; 22 | } 23 | 24 | li:nth-child(-n + 3) { 25 | display: block; 26 | } 27 | } 28 | } 29 | 30 | .card-picture { 31 | display: block; 32 | width: 100%; 33 | height: auto; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | plugins: [ 5 | require('postcss-import')({ 6 | resolve (id) { 7 | let aliasFound = false 8 | const listAlias = [ 9 | { 10 | alias: '@commonVars', 11 | path: './src/styles/vars.css' 12 | } 13 | ] 14 | 15 | listAlias.forEach(item => { 16 | if (id.match(new RegExp(item.alias, 'g'))) { 17 | aliasFound = path.resolve(__dirname, item.path, id.slice(16)) 18 | } 19 | }) 20 | 21 | return aliasFound || id 22 | } 23 | }), 24 | require('postcss-preset-env')({ 25 | stage: 2, 26 | features: { 27 | 'custom-properties': { 28 | warnings: true, 29 | preserve: false 30 | } 31 | } 32 | }), 33 | require('postcss-nested')(), 34 | require('postcss-custom-media')() 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroText: TumblrBuilder 4 | tagline: Javascript library to build custom Tumblr from API 5 | actionText: Get Started → 6 | actionLink: /guide/ 7 | features: 8 | - title: Default and custom theme 9 | details: All pages and article types have minimalist theme with the main fields and can individually be overrides 10 | - title: Extra features 11 | details: Infinite scroll on articles list, display all hashtags of articles, get related posts and increase load performance with browser storage 12 | - title: Out of the box 13 | details: TumblrBuilder can works inside a Tumblr blog and outside in your personal website, thanks to the API 14 | footer: MIT Licensed | Copyright © 2013-2020 Yoriiis 15 | --- 16 | 17 | # Easy install 18 | 19 | ```bash 20 | npm install tumblr-builder --save-dev 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## Installation 4 | 5 | First, install the library from [npm](https://www.npmjs.com/package/tumblr-builder). 6 | 7 | ::: tip Package manager 8 | In the following example we will use `npm` but `yarn` also work :heart: 9 | ::: 10 | 11 | ```bash 12 | npm install tumblr-builder --save-dev 13 | ``` 14 | 15 | ## Get an API Key 16 | 17 | TumblrBuilder use [Tumblr API v2](https://www.tumblr.com/docs/en/api/v2), to use it you must have an API key. The API key is a unique identifier that is used to authenticate requests associated with your project. 18 | 19 | First, [register your application](https://www.tumblr.com/oauth/register) and get the API key. 20 | 21 | You must include the API key with the `apiKey` parameter in the [Javascript instanciation](how-it-works.html#instanciation). 22 | -------------------------------------------------------------------------------- /src/scripts/templates/posts/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for text Tumblr post type 3 | * 4 | * @param {Object} datas Datas for the post 5 | * 6 | * @returns {String} HTML string for the template 7 | */ 8 | 9 | export default function TemplateText (datas) { 10 | /* prettier-ignore */ 11 | return ` 12 |
13 |
14 | ${datas.title} 15 | ${datas.body} 16 |
    17 | ${datas.tags.map(tag => ` 18 |
  • 19 | #${tag} 20 |
  • 21 | `).join('')} 22 |
23 |
24 |
25 | ` 26 | } 27 | -------------------------------------------------------------------------------- /src/scripts/templates/posts/link.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for link Tumblr post type 3 | * 4 | * @param {Object} datas Datas for the post 5 | * 6 | * @returns {String} HTML string for the template 7 | */ 8 | 9 | export default function TemplateLink (datas) { 10 | /* prettier-ignore */ 11 | return ` 12 | 25 | ` 26 | } 27 | -------------------------------------------------------------------------------- /src/scripts/templates/pages/tagged.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for Tumblr tagged page 3 | * 4 | * @param {Object} 5 | * @param {Object} templates List of all template pages functions 6 | * @param {Object} tag The current hashtag from the route 7 | * @param {Object} posts List of all articles to display 8 | * 9 | * @returns {String} HTML string for the template 10 | */ 11 | 12 | export default function TemplateTagged ({ templates, tag, posts }) { 13 | /* prettier-ignore */ 14 | return ` 15 | 23 | 24 |
25 | ${posts.map(post => templates[post.type](post)).join('')} 26 |
27 | ` 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/templates/posts/audio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for audio Tumblr post type 3 | * 4 | * @param {Object} datas Datas for the post 5 | * 6 | * @returns {String} HTML string for the template 7 | */ 8 | 9 | export default function TemplateAudio (datas) { 10 | /* prettier-ignore */ 11 | return ` 12 |
13 |
14 | ${datas.player} 15 |
16 |
17 | ${datas.summary} 18 |
    19 | ${datas.tags.map(tag => ` 20 |
  • 21 | #${tag} 22 |
  • 23 | `).join('')} 24 |
25 |
26 |
27 | ` 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/templates/posts/quote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for quote Tumblr post type 3 | * 4 | * @param {Object} datas Datas for the post 5 | * 6 | * @returns {String} HTML string for the template 7 | */ 8 | 9 | export default function TemplateQuote (datas) { 10 | /* prettier-ignore */ 11 | return ` 12 |
13 |
14 |
15 |

${datas.text}

16 |
${datas.source}
17 |
18 |
    19 | ${datas.tags.map(tag => ` 20 |
  • 21 | #${tag} 22 |
  • 23 | `).join('')} 24 |
25 |
26 |
27 | ` 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/templates/posts/video.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for video Tumblr post type 3 | * 4 | * @param {Object} datas Datas for the post 5 | * 6 | * @returns {String} HTML string for the template 7 | */ 8 | 9 | export default function TemplateVideo (datas) { 10 | /* prettier-ignore */ 11 | return ` 12 |
13 |
14 | ${datas.player[2].embed_code} 15 |
16 |
17 | ${datas.summary} 18 |
    19 | ${datas.tags.map(tag => ` 20 |
  • 21 | #${tag} 22 |
  • 23 | `).join('')} 24 |
25 |
26 |
27 | ` 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug report" 3 | about: Create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | ## 🐛 Bug Report 8 | 9 | Briefly describe the issue. 10 | Include a [reduced test case](https://css-tricks.com/reduced-test-cases/) if possible (CodePen or another). 11 | 12 | ## Steps to reproduce 13 | 14 | Explain in detail the exact steps necessary to reproduce the issue. 1. 2. 3. 15 | 16 | ## Results 17 | 18 | ### Expected 19 | 20 | Please describe what you expected to see. 21 | 22 | ### Actual 23 | 24 | Please describe what actually happened. 25 | 26 | ### Error output 27 | 28 | If there are any errors at all, please include them here. 29 | 30 | ## Additional information 31 | 32 | Please include any additional information necessary here. Including the following: 33 | 34 | ### versions 35 | 36 | #### TumblrBuilder 37 | 38 | What version of `TumblrBuilder` does this occur with? 39 | 40 | #### Browsers 41 | 42 | What browser are affected? 43 | 44 | #### OS 45 | 46 | What platforms (operating systems and devices) are affected? 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Yoriiis 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/scripts/templates/pages/post.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for Tumblr post page 3 | * 4 | * @param {Object} 5 | * @param {Object} templates List of all template pages functions 6 | * @param {Object} posts List of all articles to display 7 | * @param {Object} relatedPosts List of related posts for the associated article 8 | * 9 | * @returns {String} HTML string for the template 10 | */ 11 | 12 | export default function TemplatePost ({ templates, posts, relatedPosts }) { 13 | /* prettier-ignore */ 14 | return ` 15 | 23 |
24 | ${posts.map(post => templates[post.type](post)).join('')} 25 |
26 | ${relatedPosts.length ? ` 27 | 31 | ` : ''} 32 | ` 33 | } 34 | -------------------------------------------------------------------------------- /src/scripts/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get current scroll position 3 | * 4 | * @returns {Integer} Scroll position 5 | */ 6 | export function getScrollTop () { 7 | return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0 8 | } 9 | 10 | /** 11 | * Get a unique random number between min/max 12 | * 13 | * @param {*} numPicks Number of items to returns 14 | * @param {*} min Minimal value to start the random 15 | * @param {*} max Maximal value to end the random 16 | * 17 | * @returns {Array} Array of random numbers 18 | */ 19 | export function getRandoms (numPicks, min, max) { 20 | var len = max - min + 1 21 | var nums = new Array(len) 22 | var selections = [] 23 | var i = 0 24 | var j = 0 25 | 26 | if (min === 0) { 27 | if (numPicks > max + 1) numPicks = max 28 | } else { 29 | if (numPicks > max - min + 1) numPicks = max 30 | } 31 | 32 | // Initialize the array 33 | for (i = 0; i < len; i++) { 34 | nums[i] = i + min 35 | } 36 | 37 | // Randomly pick one from the array 38 | for (j = 0; j < numPicks; j++) { 39 | var index = Math.floor(Math.random() * nums.length) 40 | selections.push(nums[index]) 41 | nums.splice(index, 1) 42 | } 43 | 44 | return selections 45 | } 46 | -------------------------------------------------------------------------------- /src/scripts/templates/posts/chat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for chat Tumblr post type 3 | * 4 | * @param {Object} datas Datas for the post 5 | * 6 | * @returns {String} HTML string for the template 7 | */ 8 | 9 | export default function TemplateChat (datas) { 10 | /* prettier-ignore */ 11 | return ` 12 |
13 |
14 | ${datas.title} 15 |
    16 | ${datas.dialogue.map(line => ` 17 |
  • 18 | ${line.label}   19 | ${line.phrase} 20 |
  • 21 | `).join('')} 22 |
23 |
    24 | ${datas.tags.map(tag => ` 25 |
  • 26 | #${tag} 27 |
  • 28 | `).join('')} 29 |
30 |
31 |
32 | ` 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true 5 | }, 6 | "[postcss]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.formatOnSave": true 9 | }, 10 | "[html]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.formatOnSave": true 13 | }, 14 | "[markdown]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode", 16 | "editor.formatOnSave": true 17 | }, 18 | "[json]": { 19 | "editor.defaultFormatter": "esbenp.prettier-vscode", 20 | "editor.formatOnSave": true 21 | }, 22 | "editor.formatOnSave": false, 23 | "editor.tabSize": 4, 24 | "files.associations": { 25 | "*.css": "postcss", 26 | "*.html": "html", 27 | "*.ts": "typescript", 28 | "*.json": "json" 29 | }, 30 | "files.trimTrailingWhitespace": true, 31 | "javascript.format.enable": false, 32 | "javascript.validate.enable": false, 33 | "search.exclude": { 34 | "**/node_modules": true 35 | }, 36 | "editor.codeActionsOnSave": { 37 | "source.fixAll.eslint": true 38 | }, 39 | "editor.rulers": [100] 40 | } -------------------------------------------------------------------------------- /examples/dist/basic-usage.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s="./examples/basic-usage/basic-usage.js")}({"./examples/basic-usage/basic-usage.js":function(e,t){new window.TumblrBuilder({element:document.querySelector("#tumblr-app"),host:"tmblr-builder.tumblr.com",apiKey:"X3Hk9uvqCx5OJJVKm9AZX8uh6wf1OhLPtKK5vCJcmQUyngWabO",limitData:250,cache:!0,cacheMethod:"sessionStorage",nearBottom:350,elementsPerPage:2}).init().then(e=>{console.log(e)})}}); -------------------------------------------------------------------------------- /src/scripts/templates/pages/home.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for Tumblr home page 3 | * 4 | * @param {Object} 5 | * @param {Object} templates List of all template pages functions 6 | * @param {Object} tags List of all hashtags from all available article 7 | * @param {Object} posts List of all articles to display 8 | * 9 | * @returns {String} HTML string for the template 10 | */ 11 | 12 | export default function TemplateHome ({ templates, tags, posts }) { 13 | /* prettier-ignore */ 14 | return ` 15 | 23 |
24 |
    25 | ${tags.map(tag => ` 26 |
  • 27 | #${tag} 28 |
  • 29 | `).join('')} 30 |
  • 31 |
32 |
33 |
34 | ${posts.map(post => templates[post.type](post)).join('')} 35 |
36 | ` 37 | } 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | ## What type of PR is this? (check all applicable) 15 | 16 | - [ ] Refactor 17 | - [ ] Feature 18 | - [ ] Bug Fix 19 | - [ ] Optimization 20 | - [ ] Documentation Update 21 | 22 | ## Description 23 | 24 | Please describe the change as necessary. 25 | If it's a feature or enhancement please be as detailed as possible. 26 | If it's a bug fix, please link the issue that it fixes or describe the bug in as much detail. 27 | 28 | ## Related Tickets & Documents 29 | 30 | ## Mobile & Desktop Screenshots/Recordings (if there are UI changes) 31 | 32 | ## Added tests? 33 | 34 | - [ ] Yes 35 | - [ ] No, because they aren't needed 36 | - [ ] No, because I need help 37 | 38 | ## Added to documentation? 39 | 40 | - [ ] VuePress docs 41 | - [ ] Readme 42 | - [ ] No documentation needed 43 | 44 | ## [optional] Are there any post deployment tasks we need to perform? 45 | -------------------------------------------------------------------------------- /src/scripts/templates/posts/photo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Template for photo/photoset Tumblr post type 3 | * 4 | * @param {Object} datas Datas for the post 5 | * 6 | * @returns {String} HTML string for the template 7 | */ 8 | 9 | export default function TemplatePhoto (datas) { 10 | const isPhotoset = datas.photos.length > 1 11 | /* prettier-ignore */ 12 | return ` 13 |
14 |
15 |
    16 | ${datas.photos.map(photo => ` 17 |
  • 18 | ${datas.summary} 19 |
  • 20 | `).join('')} 21 |
22 | ${datas.summary 23 | ? `${datas.summary}` 24 | : ''} 25 |
    26 | ${datas.tags.map(tag => ` 27 |
  • 28 | #${tag} 29 |
  • 30 | `).join('')} 31 |
32 |
33 |
34 | ` 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/components/card.css: -------------------------------------------------------------------------------- 1 | @import "@commonVars"; 2 | 3 | .card { 4 | margin-bottom: 25px; 5 | position: relative; 6 | display: flex; 7 | flex-direction: column; 8 | word-wrap: break-word; 9 | border-radius: 4px; 10 | border: 1px solid var(--gray-10); 11 | 12 | &:last-child { 13 | margin-bottom: 0; 14 | } 15 | 16 | &-body { 17 | padding: 20px; 18 | } 19 | 20 | &-title { 21 | font-size: 20px; 22 | line-height: 24px; 23 | font-weight: 500; 24 | margin-bottom: 20px; 25 | display: block; 26 | } 27 | 28 | p { 29 | font-size: 16px; 30 | line-height: 24px; 31 | margin-bottom: 20px; 32 | } 33 | 34 | &-iframe { 35 | position: relative; 36 | height: 0; 37 | padding-bottom: 56.25%; 38 | 39 | iframe { 40 | width: 100%; 41 | height: 100%; 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | } 46 | } 47 | 48 | &-tags { 49 | margin-top: 10px; 50 | 51 | li { 52 | margin-top: 10px; 53 | margin-right: 10px; 54 | display: inline-block; 55 | vertical-align: top; 56 | 57 | &:last-child { 58 | margin-right: 0; 59 | } 60 | } 61 | 62 | a { 63 | color: var(--gray-65); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /netlify/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TumblrBuilder 5 | 6 | 7 | 8 | 14 | 15 | 16 |
17 |
18 | 34 | 40 | 41 |
42 |
43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /netlify/netlify.js: -------------------------------------------------------------------------------- 1 | import TumblrBuilder from '../dist/tumblr-builder' 2 | import '../dist/tumblr-builder.css' 3 | 4 | import './netlify.css' 5 | 6 | const host = window.sessionStorage.getItem('tumblr') || 'tmblr-builder.tumblr.com' 7 | 8 | const tumblr = new TumblrBuilder({ 9 | element: document.querySelector('#tumblr-app'), 10 | host, 11 | apiKey: 'ZbYkYWjthx8Uf7eUY6795MYOKUYRb9xoAXlneH7AmAtyv0LQ2x', 12 | limitData: 250, 13 | cache: true, 14 | cacheMethod: 'sessionStorage', 15 | nearBottom: 350, 16 | elementsPerPage: 2 17 | }) 18 | 19 | // Initialize the Tumblr from the instance 20 | tumblr.init().then(response => { 21 | console.log(response) 22 | 23 | // No result, redirect to a fresh home with default host 24 | if (response === false) { 25 | window.sessionStorage.removeItem('tumblr') 26 | } 27 | }) 28 | 29 | const formInputText = document.querySelector('#form-text') 30 | window.sessionStorage.setItem('tumblr', host) 31 | formInputText.value = host.split('.tumblr.com')[0] 32 | 33 | // Update the host from the form 34 | document.querySelector('.form').addEventListener('submit', e => { 35 | e.preventDefault() 36 | 37 | const inputValue = formInputText.value || false 38 | if (inputValue) { 39 | window.sessionStorage.setItem('tumblr', `${inputValue}.tumblr.com`) 40 | window.sessionStorage.removeItem('TumblrBuilderJsonData') 41 | window.location.href = '' 42 | } 43 | }) 44 | 45 | // Update the dark mode from the button 46 | const html = document.querySelector('html') 47 | html.querySelector('[data-button-darkmode]').addEventListener('click', e => { 48 | e.preventDefault() 49 | 50 | const darkMode = html.classList.contains('darkMode') 51 | if (darkMode) { 52 | html.classList.remove('darkMode') 53 | } else { 54 | html.classList.add('darkMode') 55 | } 56 | 57 | window.localStorage.setItem('tumblrDarkMode', !darkMode) 58 | }) 59 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Build a custom Tumblr theme with Javascript! 4 | 5 | ## Online playground 6 | 7 | If you're interested in playing around with TumblrBuilder, you can use the online code playground on [CodePen](https://codepen.io/yoriiis/pen/abvZWdv). 8 | 9 | ## First examples 10 | 11 | If you prefer to use your own text editor, you can also download the [repository](https://github.com/yoriiis/tumblr-builder) from Github. 12 | 13 | The project includes several examples in the `./examples/` directory. All examples described below are fully functional with all pages (home, tagged, post) and all article types (audio, chat, link, photo, quote, text, video). 14 | 15 | ### Basic usage 16 | 17 | The example includes all default templates for pages and articles. 18 | 19 | See the [Basic usage](https://github.com/yoriiis/tumblr-builder/tree/master/examples/basic-usage) example on Github. 20 | 21 | ### Custom pages 22 | 23 | The example includes custom templates for pages and default templates for articles. 24 | 25 | See the [Custom pages](https://github.com/yoriiis/tumblr-builder/tree/master/examples/custom-pages) example on Github. 26 | 27 | ### Custom posts 28 | 29 | The example includes custom templates for articles and default templates for pages. 30 | 31 | See the [Custom posts](https://github.com/yoriiis/tumblr-builder/tree/master/examples/custom-posts) example on Github. 32 | 33 | ## Tumblr contents 34 | 35 | All pages described below have their own default templates and can be individually customized with [custom pages](custom-pages.html). Same for all these article types with [custom posts](custom-posts.html). 36 | 37 | ### Tumblr pages 38 | 39 | A Tumblr blog is composed by 3 pages. 40 | 41 | - Home page 42 | - Tagged page 43 | - Post page 44 | 45 | ### Tumblr articles 46 | 47 | A Tumblr blog accepts 7 different article types. 48 | 49 | - Audio 50 | - Chat 51 | - Link 52 | - Photo 53 | - Quote 54 | - Text 55 | - Video 56 | -------------------------------------------------------------------------------- /netlify/netlify.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --gray: #80868b; 3 | --gray-10: rgba(0, 0, 0, 0.1); 4 | 5 | /* Darkmode */ 6 | --dark: #202124; 7 | --gray-darkMode: #5f6368; 8 | --transition-darkMode: 250ms ease-out; 9 | --yellow: #ffe300; 10 | } 11 | 12 | /* Form to update on Tumblr host */ 13 | .tmblr-iframe-themed .form { 14 | margin-top: 50px; 15 | } 16 | 17 | .form { 18 | text-align: right; 19 | position: relative; 20 | display: flex; 21 | height: 38px; 22 | justify-content: space-between; 23 | 24 | input { 25 | width: 100%; 26 | height: 100%; 27 | padding: 6px 12px; 28 | border-radius: 4px; 29 | font-size: 16px; 30 | background-color: inherit; 31 | border: 1px solid var(--gray-10); 32 | } 33 | 34 | .form-text { 35 | margin-bottom: 10px; 36 | margin-right: 10px; 37 | color: var(--gray); 38 | } 39 | 40 | .form-submit { 41 | cursor: pointer; 42 | color: var(--gray); 43 | width: auto; 44 | } 45 | } 46 | 47 | /* Dark mode */ 48 | html { 49 | transition: background var(--transition-darkMode); 50 | } 51 | 52 | .card  { 53 | transition: border var(--transition-darkMode); 54 | } 55 | 56 | .button-darkMode { 57 | width: 40px; 58 | height: 40px; 59 | border-radius: 50%; 60 | cursor: pointer; 61 | margin-right: 10px; 62 | 63 | &:focus { 64 | outline: none; 65 | } 66 | 67 | svg { 68 | fill: #000; 69 | transition: fill var(--transition-darkMode); 70 | } 71 | } 72 | 73 | .darkMode { 74 | background-color: var(--dark); 75 | color: var(--gray); 76 | 77 | .button-darkMode svg { 78 | fill: var(--yellow); 79 | } 80 | 81 | a, 82 | .card-tags a, 83 | .tags a { 84 | color: var(--gray); 85 | } 86 | 87 | .card, 88 | .card-linkItem, 89 | .form input { 90 | border: 1px solid var(--gray-darkMode); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2.0.0 4 | 5 | ### New features 6 | 7 | - Add documentation built with VuePress and host on Github pages 8 | - Add Webpack for resources bundling 9 | - Add Github Actions for CI 10 | - Add linter tools: MarkdownLint, Stylelint, ESLint and Prettier 11 | - Add examples: basic usage, custom pages, custom posts 12 | - Add Netlify demo 13 | - Add default template for pages and posts 14 | 15 | ### Updates 16 | 17 | - Rework in ES6 18 | 19 | ### Breaking changes 20 | 21 | - The library name is replaced by `TumblrBuilder` 22 | 23 | ## 1.8.0 24 | 25 | ### Updates 26 | 27 | - Support hash in url 28 | - Add debug mode to show log error and warning in development 29 | - Support API V1 & V2 30 | - Instanciation of the class simplified 31 | - Add method to enable/disable scrollInfini 32 | - Delete webStorage if user disable cache (avoid close tab) 33 | - Add support webStorage session and local 34 | - Add a option to use API or just use infiniteScroll 35 | - Detec IE8 by feature and disable infiniteScroll because there is a limit with Tumblr (API V1 : ajax and getJSON KO. API V2 getJSON OK). Use pagination for this browser. 36 | 37 | ### Fixes 38 | 39 | - Fix bug in loop to get data 40 | - Fix bug IE11 and popin authentification 41 | - Fix bug `getRelatedPost` and simplified call 42 | 43 | ### Removes 44 | 45 | - Remove method `getFirstPushHome` (duplicate by `getPostOfPage()`) 46 | 47 | ## 1.7.0 48 | 49 | ### Updates 50 | 51 | - Change URL of ajax request in tagged page 52 | 53 | ### Fixes 54 | 55 | - Fix infinite scroll on tagged page 56 | 57 | ## 1.6.0 58 | 59 | ### Updates 60 | 61 | - Encode url in `getTagPage()`, fix (#2) 62 | - `getRelatedPosts()` use `getTagsPost()` and sessionStorage. There is a limit with related posts, only for X first post (X = limitPostInJSON). Log "Unknown idPost" 63 | - Remove console.log 64 | 65 | ### Fixes 66 | 67 | - Fix infinite scroll disable on tag page 68 | - Fix number loop and `countMissingPost` in `getAllJSON()` 69 | 70 | ## 1.5.0 71 | 72 | ### Updates 73 | 74 | - First release of `Tumblr` 75 | - Update README 76 | -------------------------------------------------------------------------------- /src/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font: inherit; 91 | vertical-align: baseline; 92 | } 93 | 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | 109 | body { 110 | line-height: 1; 111 | } 112 | 113 | ol, 114 | ul { 115 | list-style: none; 116 | } 117 | 118 | blockquote, 119 | q { 120 | quotes: none; 121 | } 122 | 123 | blockquote::before, 124 | blockquote::after, 125 | q::before, 126 | q::after { 127 | content: ""; 128 | content: none; 129 | } 130 | 131 | table { 132 | border-collapse: collapse; 133 | border-spacing: 0; 134 | } 135 | 136 | /* Custom */ 137 | * { 138 | box-sizing: border-box; 139 | } 140 | 141 | html { 142 | -ms-text-size-adjust: 100%; 143 | -webkit-text-size-adjust: 100%; 144 | } 145 | 146 | button { 147 | background-color: transparent; 148 | border: none; 149 | } 150 | -------------------------------------------------------------------------------- /examples/custom-pages/custom-pages.js: -------------------------------------------------------------------------------- 1 | const tumblr = new window.TumblrBuilder({ 2 | element: document.querySelector('#tumblr-app'), 3 | host: 'tmblr-builder.tumblr.com', 4 | apiKey: 'X3Hk9uvqCx5OJJVKm9AZX8uh6wf1OhLPtKK5vCJcmQUyngWabO', 5 | limitData: 250, 6 | cache: true, 7 | cacheMethod: 'sessionStorage', 8 | nearBottom: 350, 9 | elementsPerPage: 2, 10 | templatesPages: { 11 | home: ({ templates, tags, posts }) => { 12 | /* prettier-ignore */ 13 | return ` 14 | Custom home page 15 | 23 |
24 |
    25 | ${tags.map(tag => ` 26 |
  • 27 | #${tag} 28 |
  • 29 | `).join('')} 30 |
  • 31 |
32 |
33 |
34 | ${posts.map(post => templates[post.type](post)).join('')} 35 |
36 | ` 37 | }, 38 | tagged: ({ templates, tag, posts }) => { 39 | /* prettier-ignore */ 40 | return ` 41 | Custom tagged page 42 | 50 | 51 |
52 | ${posts.map(post => templates[post.type](post)).join('')} 53 |
54 | ` 55 | }, 56 | post: ({ templates, posts, relatedPosts }) => { 57 | /* prettier-ignore */ 58 | return ` 59 | Custom post pages 60 | 68 |
69 | ${posts.map(post => templates[post.type](post)).join('')} 70 |
71 | ${relatedPosts.length ? ` 72 | 76 | ` : ''} 77 | ` 78 | } 79 | } 80 | }) 81 | 82 | // Initialize the Tumblr from the instance 83 | tumblr.init().then(response => { 84 | console.log(response) 85 | }) 86 | -------------------------------------------------------------------------------- /netlify/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 5 | const TerserPlugin = require('terser-webpack-plugin') 6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 7 | 8 | module.exports = (env, argv) => { 9 | const isProduction = argv.mode === 'production' 10 | 11 | return { 12 | watch: !isProduction, 13 | entry: { 14 | netlify: `${path.resolve(__dirname, './netlify.js')}` 15 | }, 16 | watchOptions: { 17 | ignored: /node_modules/ 18 | }, 19 | devtool: !isProduction ? 'source-map' : 'none', 20 | output: { 21 | path: path.resolve(__dirname, './dist'), 22 | filename: '[name].js', 23 | sourceMapFilename: '[file].map' 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.js$/, 29 | include: [path.resolve(__dirname, './')], 30 | use: [ 31 | { 32 | loader: 'babel-loader' 33 | } 34 | ] 35 | }, 36 | { 37 | test: /\.css$/, 38 | include: [path.resolve(__dirname, './'), path.resolve(__dirname, '../dist')], 39 | use: [ 40 | MiniCssExtractPlugin.loader, 41 | { 42 | loader: 'css-loader' 43 | }, 44 | { 45 | loader: 'postcss-loader', 46 | options: { 47 | config: { 48 | path: path.resolve(__dirname, './') 49 | } 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | }, 56 | plugins: [ 57 | new ProgressBarPlugin(), 58 | new MiniCssExtractPlugin({ 59 | filename: '[name].css', 60 | chunkFilename: '[name].css' 61 | }), 62 | new webpack.optimize.ModuleConcatenationPlugin() 63 | ], 64 | stats: { 65 | assets: true, 66 | colors: true, 67 | hash: false, 68 | timings: true, 69 | chunks: false, 70 | chunkModules: false, 71 | modules: false, 72 | children: false, 73 | entrypoints: false, 74 | excludeAssets: /.map$/, 75 | assetsSort: '!size' 76 | }, 77 | optimization: { 78 | minimizer: [ 79 | new TerserPlugin({ 80 | extractComments: false, 81 | cache: true, 82 | parallel: true, 83 | sourceMap: false, 84 | terserOptions: { 85 | extractComments: 'all', 86 | compress: { 87 | drop_console: false 88 | }, 89 | mangle: true 90 | } 91 | }), 92 | new OptimizeCSSAssetsPlugin({}) 93 | ], 94 | namedModules: true, 95 | removeAvailableModules: true, 96 | removeEmptyChunks: true, 97 | mergeDuplicateChunks: true, 98 | occurrenceOrder: true, 99 | providedExports: false, 100 | splitChunks: false 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | 6 | const TerserPlugin = require('terser-webpack-plugin') 7 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 8 | 9 | module.exports = (env, argv) => { 10 | const isProduction = argv.mode === 'production' 11 | 12 | return { 13 | watch: !isProduction, 14 | entry: { 15 | 'tumblr-builder': './src/scripts/tumblr-builder.js' 16 | }, 17 | watchOptions: { 18 | ignored: /node_modules/ 19 | }, 20 | devtool: !isProduction ? 'source-map' : 'none', 21 | output: { 22 | path: path.resolve(__dirname, './dist'), 23 | publicPath: '/dist/', 24 | filename: '[name].js', 25 | sourceMapFilename: '[file].map', 26 | libraryTarget: 'umd', 27 | libraryExport: 'default', 28 | library: 'TumblrBuilder' 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.js$/, 34 | include: path.resolve(__dirname, './src'), 35 | use: [ 36 | { 37 | loader: 'babel-loader' 38 | } 39 | ] 40 | }, 41 | { 42 | test: /\.css$/, 43 | include: [path.resolve(__dirname, './src')], 44 | use: [ 45 | MiniCssExtractPlugin.loader, 46 | { loader: 'css-loader' }, 47 | { 48 | loader: 'postcss-loader', 49 | options: { 50 | config: { 51 | path: path.resolve(__dirname, './') 52 | } 53 | } 54 | } 55 | ] 56 | } 57 | ] 58 | }, 59 | plugins: [ 60 | new ProgressBarPlugin(), 61 | new webpack.optimize.ModuleConcatenationPlugin(), 62 | new MiniCssExtractPlugin({ 63 | filename: '[name].css', 64 | chunkFilename: '[name].css' 65 | }) 66 | ], 67 | stats: { 68 | assets: true, 69 | colors: true, 70 | hash: false, 71 | timings: true, 72 | chunks: false, 73 | chunkModules: false, 74 | modules: false, 75 | children: false, 76 | entrypoints: false, 77 | excludeAssets: /.map$/, 78 | assetsSort: '!size' 79 | }, 80 | optimization: { 81 | minimizer: [ 82 | new TerserPlugin({ 83 | extractComments: false, 84 | cache: true, 85 | parallel: true, 86 | sourceMap: false, 87 | terserOptions: { 88 | extractComments: 'all', 89 | compress: { 90 | drop_console: false 91 | }, 92 | mangle: true 93 | } 94 | }), 95 | new OptimizeCSSAssetsPlugin({}) 96 | ], 97 | namedModules: true, 98 | removeAvailableModules: true, 99 | removeEmptyChunks: true, 100 | mergeDuplicateChunks: true, 101 | occurrenceOrder: true, 102 | providedExports: false, 103 | splitChunks: false 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /examples/dist/custom-pages.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function n(a){if(e[a])return e[a].exports;var s=e[a]={i:a,l:!1,exports:{}};return t[a].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=t,n.c=e,n.d=function(t,e,a){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:a})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(n.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var s in t)n.d(a,s,function(e){return t[e]}.bind(null,s));return a},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s="./examples/custom-pages/custom-pages.js")}({"./examples/custom-pages/custom-pages.js":function(t,e){new window.TumblrBuilder({element:document.querySelector("#tumblr-app"),host:"tmblr-builder.tumblr.com",apiKey:"X3Hk9uvqCx5OJJVKm9AZX8uh6wf1OhLPtKK5vCJcmQUyngWabO",limitData:250,cache:!0,cacheMethod:"sessionStorage",nearBottom:350,elementsPerPage:2,templatesPages:{home:({templates:t,tags:e,posts:n})=>`\n\t\t\t\tCustom home page\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t
    \n\t\t\t\t\t\t${e.map(t=>`\n\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t#${t}\n\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
  • \n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t${n.map(e=>t[e.type](e)).join("")}\n\t\t\t\t
\n\t\t\t`,tagged:({templates:t,tag:e,posts:n})=>`\n\t\t\t\tCustom tagged page\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t${n.map(e=>t[e.type](e)).join("")}\n\t\t\t\t
\n\t\t\t`,post:({templates:t,posts:e,relatedPosts:n})=>`\n\t\t\t\tCustom post pages\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t${e.map(e=>t[e.type](e)).join("")}\n\t\t\t\t
\n\t\t\t\t${n.length?`\n\t\t\t\t\t\n\t\t\t\t`:""}\n \t\t`}}).init().then(t=>{console.log(t)})}}); -------------------------------------------------------------------------------- /examples/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | const ProgressBarPlugin = require('progress-bar-webpack-plugin') 5 | const TerserPlugin = require('terser-webpack-plugin') 6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 7 | 8 | module.exports = (env, argv) => { 9 | const isProduction = argv.mode === 'production' 10 | 11 | return { 12 | watch: !isProduction, 13 | entry: { 14 | 'basic-usage': `${path.resolve(__dirname, './basic-usage/basic-usage.js')}`, 15 | 'custom-pages': `${path.resolve(__dirname, './custom-pages/custom-pages.js')}`, 16 | 'custom-posts': `${path.resolve(__dirname, './custom-posts/custom-posts.js')}` 17 | }, 18 | watchOptions: { 19 | ignored: /node_modules/ 20 | }, 21 | devtool: !isProduction ? 'source-map' : 'none', 22 | output: { 23 | path: path.resolve(__dirname, './dist'), 24 | filename: '[name].js', 25 | sourceMapFilename: '[file].map' 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.js$/, 31 | include: [path.resolve(__dirname, './')], 32 | use: [ 33 | { 34 | loader: 'babel-loader' 35 | } 36 | ] 37 | }, 38 | { 39 | test: /\.css$/, 40 | include: [path.resolve(__dirname, './'), path.resolve(__dirname, '../dist')], 41 | use: [ 42 | MiniCssExtractPlugin.loader, 43 | { 44 | loader: 'css-loader' 45 | }, 46 | { 47 | loader: 'postcss-loader', 48 | options: { 49 | config: { 50 | path: path.resolve(__dirname, './') 51 | } 52 | } 53 | } 54 | ] 55 | } 56 | ] 57 | }, 58 | plugins: [ 59 | new ProgressBarPlugin(), 60 | new MiniCssExtractPlugin({ 61 | filename: '[name].css', 62 | chunkFilename: '[name].css' 63 | }), 64 | new webpack.optimize.ModuleConcatenationPlugin() 65 | ], 66 | stats: { 67 | assets: true, 68 | colors: true, 69 | hash: false, 70 | timings: true, 71 | chunks: false, 72 | chunkModules: false, 73 | modules: false, 74 | children: false, 75 | entrypoints: false, 76 | excludeAssets: /.map$/, 77 | assetsSort: '!size' 78 | }, 79 | optimization: { 80 | minimizer: [ 81 | new TerserPlugin({ 82 | extractComments: false, 83 | cache: true, 84 | parallel: true, 85 | sourceMap: false, 86 | terserOptions: { 87 | extractComments: 'all', 88 | compress: { 89 | drop_console: false 90 | }, 91 | mangle: true 92 | } 93 | }), 94 | new OptimizeCSSAssetsPlugin({}) 95 | ], 96 | namedModules: true, 97 | removeAvailableModules: true, 98 | removeEmptyChunks: true, 99 | mergeDuplicateChunks: true, 100 | occurrenceOrder: true, 101 | providedExports: false, 102 | splitChunks: false 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | base: '/tumblr-builder/', 3 | dest: 'docs/site', 4 | markdown: { 5 | lineNumbers: true 6 | }, 7 | title: 'TumblrBuilder', 8 | description: 'Javascript library to build custom Tumblr from API', 9 | head: [ 10 | ['link', { rel: 'manifest', href: '/manifest.json' }], 11 | ['link', { rel: 'shortcut icon', type: 'image/png', href: '/favicon.ico' }], 12 | ['meta', { name: 'theme-color', content: '#001935' }], 13 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], 14 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], 15 | ['link', { rel: 'apple-touch-icon', href: 'icons/apple-touch-icon-152x152.png' }], 16 | [ 17 | 'meta', 18 | { name: 'msapplication-TileImage', content: 'icons/msapplication-icon-144x144.png' } 19 | ], 20 | 21 | ['meta', { name: 'msapplication-TileColor', content: '#001935' }] 22 | ], 23 | plugins: [ 24 | [ 25 | '@vuepress/pwa', 26 | { 27 | serviceWorker: true, 28 | updatePopup: true 29 | } 30 | ], 31 | '@vuepress/back-to-top', 32 | '@vuepress/nprogress' 33 | ], 34 | themeConfig: { 35 | repo: 'yoriiis/tumblr-builder', 36 | repoLabel: 'Github', 37 | docsDir: 'docs', 38 | docsBranch: 'master', 39 | editLinks: true, 40 | editLinkText: 'Edit this page', 41 | smoothScroll: true, 42 | nextLinks: true, 43 | prevLinks: true, 44 | lastUpdated: 'Last Updated', 45 | search: true, 46 | searchMaxSuggestions: 10, 47 | searchPlaceholder: 'Search...', 48 | algolia: { 49 | apiKey: '02b27ef69549260976707846ce013194', 50 | indexName: 'tumblr-builder' 51 | }, 52 | activeHeaderLinks: false, 53 | displayAllHeaders: false, 54 | nav: [ 55 | { text: 'Guide', link: '/guide/' }, 56 | { 57 | text: 'Demo', 58 | items: [ 59 | { 60 | text: 'Tumblr', 61 | link: 'http://tmblr-builder.tumblr.com' 62 | }, 63 | { text: 'Out of the box', link: 'https://tumblr-builder.netlify.app' } 64 | ] 65 | }, 66 | { 67 | text: 'Learn more', 68 | items: [ 69 | { 70 | text: 'Tumblr', 71 | items: [ 72 | { 73 | text: 'API', 74 | link: 'https://www.tumblr.com/docs/en/api/v2' 75 | }, 76 | { 77 | text: 'Custom theme', 78 | link: 'https://www.tumblr.com/docs/en/custom_themes' 79 | } 80 | ] 81 | }, 82 | { 83 | text: 'Miscellaneous', 84 | items: [ 85 | { 86 | text: 'Changelog', 87 | link: 88 | 'https://github.com/yoriiis/tumblr-builder/blob/master/CHANGELOG.md' 89 | } 90 | ] 91 | } 92 | ] 93 | } 94 | ], 95 | sidebar: { 96 | '/guide/': [ 97 | { 98 | title: 'Guide', 99 | collapsable: false, 100 | sidebarDepth: 2, 101 | children: [ 102 | '', 103 | ['getting-started', 'Getting started'], 104 | ['how-it-works', 'How it works'], 105 | ['available-methods', 'Available methods'], 106 | ['deploying', 'Deploying'] 107 | ] 108 | }, 109 | { 110 | title: 'Advanced', 111 | collapsable: false, 112 | sidebarDepth: 2, 113 | children: [ 114 | ['custom-pages', 'Custom pages'], 115 | ['custom-posts', 'Custom posts'] 116 | ] 117 | } 118 | ] 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /docs/guide/available-methods.md: -------------------------------------------------------------------------------- 1 | # Available methods 2 | 3 | List of functions exposed by the instance. 4 | 5 | ::: tip API research 6 | Functions `getAllTags` and `getRelatedPosts` executes research in the collection of articles get from the API according to the [limitData](how-it-works.html#limitdata) option. The limit can be adjust to fit your need. 7 | ::: 8 | 9 | ## init 10 | 11 | - Return: `Promise` 12 | 13 | The function build the app and returns a promise. **API dependents functions must be called when `init` function is ready.** 14 | 15 | ```javascript 16 | tumblr.init().then(response => { 17 | // App is render and ready 18 | }); 19 | ``` 20 | 21 | Since `init()` returns a promise, you can achieve the same as the above using the new [ES2017 async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) syntax: 22 | 23 | ```javascript 24 | const response = await tumblr.init(); 25 | ``` 26 | 27 | The function exposes a `response` object with datas from the API including following fields. 28 | 29 | ### response.totalPosts 30 | 31 | - Type: `Integer` 32 | - Default: `0` 33 | 34 | The total of articles available on the Tumblr blog, according to the [host](how-it-works.html#host) option. 35 | 36 | ### response.posts 37 | 38 | - Type: `Array` 39 | - Default: `[]` 40 | 41 | List of articles datas returns by the app. 42 | 43 | ### response.tags 44 | 45 | - Type: `Array` 46 | - Default: `[]` 47 | 48 | List of all hashtags for all articles requested by the app. 49 | 50 | ## getAllTags 51 | 52 | - Return: `Array` 53 | 54 | The function get the list of all hashtags from all available articles. 55 | 56 | ```javascript 57 | tumblr.getAllTags(); 58 | ``` 59 | 60 | ## getRelatedPosts 61 | 62 | - Return: `Array` 63 | 64 | The function get related posts according to a specific post. 65 | 66 | ```javascript 67 | tumblr.getRelatedPosts({ 68 | postId, 69 | tags, 70 | limit, 71 | ignoreTags 72 | }); 73 | ``` 74 | 75 | The function has an object parameter with following fields. 76 | 77 | ### postId 78 | 79 | - Type: `String` 80 | - Default: `''` 81 | 82 | Tells the function the id of the associated article. 83 | 84 | ### tags 85 | 86 | - Type: `Array` 87 | - Default: `[]` 88 | 89 | Tells the function the list of hashtags for the associated article. 90 | 91 | ### limit 92 | 93 | - Type: `Integer` 94 | - Default: `3` 95 | 96 | Tells the function the limit of results to return. 97 | 98 | ### ignoreTags 99 | 100 | - Type: `Array` 101 | - Default: `[]` 102 | 103 | Tells the function the list of hashtags to ignore for the research. 104 | 105 | ## getRoute 106 | 107 | - Return: `String` 108 | 109 | The function get the current hash from the route. 110 | 111 | ```javascript 112 | tumblr.getRoute(); 113 | ``` 114 | 115 | ## getPageType 116 | 117 | - Return: `String||null` 118 | - Possible value: `'home'` `'tagged'` `'post'` `null` 119 | 120 | The function get the page type from the route. 121 | 122 | ```javascript 123 | tumblr.getPageType(); 124 | ``` 125 | 126 | ## getPostIdFromRoute 127 | 128 | - Return: `String` 129 | 130 | The function get the post id from the route (`/post/` route only). 131 | 132 | ```javascript 133 | tumblr.getPostIdFromRoute(); 134 | ``` 135 | 136 | ## getHashTagFromRoute 137 | 138 | - Return: `String` 139 | 140 | The function get the hashtag from the route (`/tagged/` route only). 141 | 142 | ```javascript 143 | tumblr.getHashTagFromRoute(); 144 | ``` 145 | -------------------------------------------------------------------------------- /docs/guide/how-it-works.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | Now dependencies and API key are done, let's write our first custom Tumblr blog. 4 | 5 | ## HTML structure 6 | 7 | Create the container for the app with a selector easily accessible. 8 | 9 | ```html 10 |
11 | 12 |
13 | ``` 14 | 15 | ## Initialize 16 | 17 | Then, we will initialize the constructor. **Highlight lines are mandatory parameters.** 18 | 19 | ```javascript{2,3,4} 20 | const tumblr = new TumblrBuilder({ 21 | element: null, 22 | host: "", 23 | apiKey: "", 24 | limitData: 250, 25 | cache: false, 26 | cacheMethod: "sessionStorage", 27 | nearBottom: 350, 28 | elementsPerPage: 20, 29 | templatesPages: {}, 30 | templatesPosts: {} 31 | }); 32 | ``` 33 | 34 | Next, create the Tumblr app. 35 | 36 | ```javascript 37 | tumblr.init().then(response => { 38 | // App is render and ready 39 | }); 40 | ``` 41 | 42 | ## Options 43 | 44 | ### element 45 | 46 | - Type: `HTMLElement` 47 | - Default: `null` 48 | 49 | The HTML element where the app will render. 50 | 51 | ### host 52 | 53 | - Type: `String` 54 | - Default: `''` 55 | 56 | The Tumblr blog hostname, for example `.tumblr.com`. The API will fetch datas from this blog. 57 | 58 | ### apiKey 59 | 60 | - Type: `String` 61 | - Default: `''` 62 | 63 | The Tumblr API key for API usage. 64 | 65 | ### limitData 66 | 67 | - Type: `Integer` 68 | - Default: `250` 69 | 70 | Number of articles to get from the API. The articles list is used to extract a list of all hashtags and find related articles relations by hashtags. 71 | 72 | ::: tip Use the browser storage 73 | To decrease the number of API requests and improve load performance, use the `cache` option to store all datas in the browser storage. 74 | ::: 75 | 76 | ### cache 77 | 78 | - Type: `Boolean` 79 | - Default: `false` 80 | 81 | Tell the library whether to store API datas in the browser storage. This option improve performance, because app retrieves datas from the browser storage and no API requests are executed on the home page. 82 | 83 | Infinite scroll will used in priority the datas from the browser storage for firsts scrolling requests if available. 84 | 85 | ::: warning Cache priotity 86 | If `cache` option is enabled the app will used datas from the browser storage in priority. To see the lastest updates to the app while contributing to Tumblr, you will probably need to delete datas from browser storage or use incognito navigation. 87 | ::: 88 | 89 | ### cacheMethod 90 | 91 | - Type: `String` 92 | - Default: `'sessionStorage'` 93 | - Possible value: `'sessionStorage'` `'localStorage'` 94 | 95 | Tell the method to use for the browser storage. 96 | 97 | ### nearBottom 98 | 99 | - Type: `Integer` 100 | - Default: `350` 101 | 102 | Tell from how many pixels from the bottom of the page the infinite scroll will trigger new requests. 103 | 104 | ### elementsPerPage 105 | 106 | - Type: `Integer` 107 | - Default: `20` 108 | 109 | Tell how many articles are display by page, for the first page request and next page requests with infinite scroll. 110 | 111 | ### templatesPages 112 | 113 | - Type: `Object` 114 | - Default: `{}` 115 | - Possible value: `{home, tagged, post}` 116 | 117 | Tell whether to override default templates for pages (home, tagged, post). 118 | 119 | Example of implementation: 120 | 121 | ```javascript 122 | new TumblrBuilder({ 123 | templatesPages: { 124 | home: ({ templates, tags, posts }) => {}, 125 | tagged: ({ templates, tag, posts }) => {}, 126 | post: ({ templates, posts, relatedPosts }) => {} 127 | } 128 | }); 129 | ``` 130 | 131 | More information about [custom pages](custom-pages.html). 132 | 133 | ### templatesPosts 134 | 135 | - Type: `Object` 136 | - Default: `{}` 137 | - Possible value: `{audio, chat, link, photo, quote, text, video}` 138 | 139 | Tell whether to override default templates for article (audio, chat, link, photo, quote, text, video). 140 | 141 | Example of implementation: 142 | 143 | ```javascript 144 | new TumblrBuilder({ 145 | templatesPosts: { 146 | audio: datas => {}, 147 | chat: datas => {}, 148 | link: datas => {}, 149 | photo: datas => {}, 150 | quote: datas => {}, 151 | text: datas => {}, 152 | video: datas => {} 153 | } 154 | }); 155 | ``` 156 | 157 | More information about [custom posts](custom-posts.html). 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tumblr-builder", 3 | "version": "2.0.0", 4 | "description": "Javascript library to build custom Tumblr from API", 5 | "keywords": [ 6 | "tumblr", 7 | "builder", 8 | "tumblr api", 9 | "tumblr custom theme", 10 | "tumblr custom pages", 11 | "tumblr custom posts", 12 | "get related posts", 13 | "infinite scroll" 14 | ], 15 | "homepage": "https://github.com/yoriiis/tumblr-builder", 16 | "bugs": "https://github.com/yoriiis/tumblr-builder/issues", 17 | "repository": "https://github.com/yoriiis/tumblr-builder.git", 18 | "license": "MIT", 19 | "author": "Yoriiis", 20 | "main": "dist/tumblr.js", 21 | "scripts": { 22 | "build": "rm -rf ./dist && webpack --mode=production", 23 | "build:all": "npm run build && npm run build:examples && npm run build:netlify", 24 | "build:examples": "rm -rf ./examples/dist && webpack --config=./examples/webpack.config.js --mode=production", 25 | "build:netlify": "rm -rf ./netlify/dist && webpack --config=./netlify/webpack.config.js --mode=production", 26 | "docs:build": "vuepress build docs", 27 | "docs:dev": "vuepress dev docs --host localhost", 28 | "start": "webpack --mode=development", 29 | "start:examples": "webpack --config=./examples/webpack.config.js --mode=development", 30 | "start:netlify": "webpack --config=./netlify/webpack.config.js --mode=development", 31 | "test": "npm run test:eslint && npm run test:stylelint npm run test:markdown", 32 | "test:eslint": "eslint . --ignore-pattern dist --ignore-pattern docs", 33 | "test:markdown": "markdownlint '**/*.md' --ignore node_modules --ignore CHANGELOG.md", 34 | "test:stylelint": "stylelint './src/**/*.css' './examples/**/*.css' --ignore-pattern examples/dist" 35 | }, 36 | "babel": { 37 | "plugins": [ 38 | "babel-plugin-dynamic-import-node", 39 | "@babel/plugin-proposal-class-properties" 40 | ], 41 | "presets": [ 42 | [ 43 | "@babel/preset-env", 44 | { 45 | "targets": { 46 | "node": "12.14.0" 47 | } 48 | } 49 | ] 50 | ] 51 | }, 52 | "browserslist": [ 53 | "last 5 version" 54 | ], 55 | "prettier": { 56 | "printWidth": 100, 57 | "overrides": [ 58 | { 59 | "files": "*.md", 60 | "options": { 61 | "proseWrap": "preserve", 62 | "tabWidth": 2, 63 | "useTabs": false 64 | } 65 | } 66 | ] 67 | }, 68 | "eslintConfig": { 69 | "env": { 70 | "browser": true, 71 | "es6": true, 72 | "node": true 73 | }, 74 | "parser": "babel-eslint", 75 | "parserOptions": { 76 | "ecmaFeatures": { 77 | "impliedStrict": true, 78 | "experimentalObjectRestSpread": true 79 | }, 80 | "ecmaVersion": 6, 81 | "sourceType": "module" 82 | }, 83 | "extends": "standard", 84 | "rules": { 85 | "indent": [ 86 | "error", 87 | "tab", 88 | { 89 | "ignoredNodes": [ 90 | "TemplateLiteral > *" 91 | ] 92 | } 93 | ], 94 | "no-console": 0, 95 | "no-tabs": 0 96 | }, 97 | "globals": { 98 | "document": false, 99 | "window": false 100 | } 101 | }, 102 | "stylelint": { 103 | "extends": "stylelint-config-standard", 104 | "rules": { 105 | "indentation": 4, 106 | "unit-whitelist": [ 107 | "%", 108 | "px", 109 | "fr", 110 | "ms" 111 | ] 112 | } 113 | }, 114 | "dependencies": {}, 115 | "devDependencies": { 116 | "@babel/core": "7.9.0", 117 | "@babel/plugin-proposal-class-properties": "7.8.3", 118 | "@babel/preset-env": "7.9.0", 119 | "@vuepress/plugin-back-to-top": "1.4.1", 120 | "@vuepress/plugin-nprogress": "1.4.1", 121 | "@vuepress/plugin-pwa": "1.4.1", 122 | "babel-eslint": "10.0.3", 123 | "babel-loader": "8.0.6", 124 | "babel-plugin-dynamic-import-node": "2.3.0", 125 | "css-loader": "3.4.2", 126 | "eslint": "6.8.0", 127 | "eslint-config-standard": "14.1.0", 128 | "eslint-plugin-import": "2.20.0", 129 | "eslint-plugin-node": "11.0.0", 130 | "eslint-plugin-promise": "4.2.1", 131 | "eslint-plugin-standard": "4.0.1", 132 | "expose-loader": "0.7.5", 133 | "file-loader": "5.0.2", 134 | "markdownlint-cli": "0.21.0", 135 | "mini-css-extract-plugin": "0.4.1", 136 | "netlify-cli": "^2.47.0", 137 | "npm-force-resolutions": "0.0.3", 138 | "optimize-css-assets-webpack-plugin": "5.0.3", 139 | "postcss-custom-media": "7.0.8", 140 | "postcss-import": "12.0.1", 141 | "postcss-loader": "3.0.0", 142 | "postcss-nested": "4.2.1", 143 | "postcss-preset-env": "6.7.0", 144 | "prettier-eslint": "9.0.1", 145 | "prettier-stylelint": "0.4.2", 146 | "progress-bar-webpack-plugin": "2.1.0", 147 | "style-loader": "1.1.3", 148 | "stylefmt": "6.0.3", 149 | "stylelint": "13.0.0", 150 | "stylelint-config-recommended": "3.0.0", 151 | "stylelint-config-standard": "19.0.0", 152 | "terser-webpack-plugin": "2.3.2", 153 | "vuepress": "1.4.1", 154 | "webpack": "4.41.5", 155 | "webpack-cli": "3.3.10", 156 | "webpack-manifest-plugin": "2.2.0" 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /examples/custom-posts/custom-posts.js: -------------------------------------------------------------------------------- 1 | const tumblr = new window.TumblrBuilder({ 2 | element: document.querySelector('#tumblr-app'), 3 | host: 'tmblr-builder.tumblr.com', 4 | apiKey: 'X3Hk9uvqCx5OJJVKm9AZX8uh6wf1OhLPtKK5vCJcmQUyngWabO', 5 | limitData: 250, 6 | cache: true, 7 | cacheMethod: 'sessionStorage', 8 | nearBottom: 350, 9 | elementsPerPage: 2, 10 | templatesPosts: { 11 | audio: datas => { 12 | /* prettier-ignore */ 13 | return ` 14 |
15 |
16 | ${datas.player} 17 |
18 |
19 | ${datas.summary} 20 |
    21 | ${datas.tags.map(tag => ` 22 |
  • 23 | #${tag} 24 |
  • 25 | `).join('')} 26 |
27 |
28 |
29 | ` 30 | }, 31 | chat: datas => { 32 | /* prettier-ignore */ 33 | return ` 34 |
35 |
36 | ${datas.title} 37 |
    38 | ${datas.dialogue.map(line => ` 39 |
  • 40 | ${line.label}   41 | ${line.phrase} 42 |
  • 43 | `).join('')} 44 |
45 |
    46 | ${datas.tags.map(tag => ` 47 |
  • 48 | #${tag} 49 |
  • 50 | `).join('')} 51 |
52 |
53 |
54 | ` 55 | }, 56 | link: datas => { 57 | /* prettier-ignore */ 58 | return ` 59 | 72 | ` 73 | }, 74 | photo: datas => { 75 | const isPhotoset = datas.photos.length > 1 76 | /* prettier-ignore */ 77 | return ` 78 |
79 |
80 |
    81 | ${datas.photos.map(photo => ` 82 |
  • 83 | ${datas.summary} 84 |
  • 85 | `).join('')} 86 |
87 | ${datas.summary 88 | ? `${datas.summary}` 89 | : ''} 90 |
    91 | ${datas.tags.map(tag => ` 92 |
  • 93 | #${tag} 94 |
  • 95 | `).join('')} 96 |
97 |
98 |
99 | ` 100 | }, 101 | quote: datas => { 102 | /* prettier-ignore */ 103 | return ` 104 |
105 |
106 |
107 |

${datas.text}

108 |
${datas.source}
109 |
110 |
    111 | ${datas.tags.map(tag => ` 112 |
  • 113 | #${tag} 114 |
  • 115 | `).join('')} 116 |
117 |
118 |
119 | ` 120 | }, 121 | text: datas => { 122 | /* prettier-ignore */ 123 | return ` 124 |
125 |
126 | ${datas.title} 127 | ${datas.body} 128 |
    129 | ${datas.tags.map(tag => ` 130 |
  • 131 | #${tag} 132 |
  • 133 | `).join('')} 134 |
135 |
136 |
137 | ` 138 | }, 139 | video: datas => { 140 | /* prettier-ignore */ 141 | return ` 142 |
143 |
144 | ${datas.player[2].embed_code} 145 |
146 |
147 | ${datas.summary} 148 |
    149 | ${datas.tags.map(tag => ` 150 |
  • 151 | #${tag} 152 |
  • 153 | `).join('')} 154 |
155 |
156 |
157 | ` 158 | } 159 | } 160 | }) 161 | 162 | // Initialize the Tumblr from the instance 163 | tumblr.init().then(response => { 164 | console.log(response) 165 | }) 166 | -------------------------------------------------------------------------------- /examples/dist/custom-posts.js: -------------------------------------------------------------------------------- 1 | !function(t){var n={};function a(s){if(n[s])return n[s].exports;var e=n[s]={i:s,l:!1,exports:{}};return t[s].call(e.exports,e,e.exports,a),e.l=!0,e.exports}a.m=t,a.c=n,a.d=function(t,n,s){a.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:s})},a.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},a.t=function(t,n){if(1&n&&(t=a(t)),8&n)return t;if(4&n&&"object"==typeof t&&t&&t.__esModule)return t;var s=Object.create(null);if(a.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var e in t)a.d(s,e,function(n){return t[n]}.bind(null,e));return s},a.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return a.d(n,"a",n),n},a.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},a.p="",a(a.s="./examples/custom-posts/custom-posts.js")}({"./examples/custom-posts/custom-posts.js":function(t,n){new window.TumblrBuilder({element:document.querySelector("#tumblr-app"),host:"tmblr-builder.tumblr.com",apiKey:"X3Hk9uvqCx5OJJVKm9AZX8uh6wf1OhLPtKK5vCJcmQUyngWabO",limitData:250,cache:!0,cacheMethod:"sessionStorage",nearBottom:350,elementsPerPage:2,templatesPosts:{audio:t=>`\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t${t.player}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t${t.summary}\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t${t.tags.map(n=>`\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t\t#${n}\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t`,chat:t=>`\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t${t.title}\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t${t.dialogue.map(t=>`\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t\t${t.label}  \n\t\t\t\t\t\t\t\t\t\t${t.phrase}\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t${t.tags.map(t=>`\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t\t#${t}\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t`,link:t=>`\n\t\t\t\t\n\t\t\t`,photo:t=>`\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t${t.photos.map(n=>`\n\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t${t.summary}\n\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
\n\t\t\t\t\t\t${t.summary?`${t.summary}`:""}\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t${t.tags.map(t=>`\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t\t#${t}\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t`,quote:t=>`\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t\t

${t.text}

\n\t\t\t\t\t\t\t
${t.source}
\n\t\t\t\t\t\t
\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t${t.tags.map(t=>`\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t\t#${t}\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t`,text:t=>`\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t${t.title}\n\t\t\t\t\t\t${t.body}\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t${t.tags.map(t=>`\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t\t#${t}\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t`,video:t=>`\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t${t.player[2].embed_code}\n\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t\t\t${t.summary}\n\t\t\t\t\t\t
    \n\t\t\t\t\t\t\t${t.tags.map(t=>`\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t\t\t\t#${t}\n\t\t\t\t\t\t\t\t\t
  • \n\t\t\t\t\t\t\t`).join("")}\n\t\t\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t
\n\t\t\t`}}).init().then(t=>{console.log(t)})}}); -------------------------------------------------------------------------------- /docs/guide/custom-pages.md: -------------------------------------------------------------------------------- 1 | # Custom pages 2 | 3 | Need to customize the HTML for the home page, tagged page or post page? The `templatesPages` option is for you :rocket: 4 | 5 | On app initialize, add the `templatesPages` parameter with pages you need to individually customized. The others pages will automatically used their default template. 6 | 7 | ::: tip Override default templates 8 | To simply override the default template for a specific page use `templatesPages.` parameter. The `` placeholder must be replaced by the following values: `home` `tagged` `post`. 9 | ::: 10 | 11 | ::: details Prettier ignore comment 12 | To prevent issue with Prettier and template literals indents, following examples are preceded by `/* prettier-ignore */`. [More information on Prettier](https://prettier.io/docs/en/ignore.html). 13 | ::: 14 | 15 | ## Home page 16 | 17 | ### Default home page 18 | 19 | The default template used for the home page is described below. The function can be replaced with the `templatesPages.home` parameter. The `data-infinite-scroll` attribute is used for the infinite scroll feature. 20 | 21 | ```javascript 22 | function home ({ templates, tags, posts }) => { 23 | /* prettier-ignore */ 24 | return ` 25 | 33 |
34 |
    35 | ${tags.map(tag => ` 36 |
  • 37 | #${tag} 38 |
  • 39 | `).join('')} 40 |
  • 41 |
42 |
43 |
44 | ${posts.map(post => templates[post.type](post)).join('')} 45 |
46 | `; 47 | } 48 | ``` 49 | 50 | The function exposes following parameters: 51 | 52 | #### home ({ templates }) 53 | 54 | - Type: `Object` 55 | - Default: `{}` 56 | 57 | List of all template pages functions (audio, chat, link, photo, quote, text, video). All defaults functions are automatically used if `templatesPages` object is not overrides them. 58 | 59 | #### home ({ tags }) 60 | 61 | - Type: `Array` 62 | - Default: `[]` 63 | 64 | List of all hashtags from all available article, according to the [limitData](how-it-works.html#limitdata) option. 65 | 66 | #### home ({ posts }) 67 | 68 | - Type: `Array` 69 | - Default: `[]` 70 | 71 | List of all articles to display on the first page (before the first infinite scroll), according to the [elementsPerPage](how-it-works.html#elementsperpage) option. 72 | 73 | ### Custom home page 74 | 75 | To customize the home page template, write your own `customHomePage` function inspired by the `home` function above. 76 | 77 | ```javascript 78 | function customHomePage({ templates, tags, posts }) { 79 | return ` 80 | 81 | `; 82 | } 83 | ``` 84 | 85 | Next, use the `templatesPages.home` parameter to passed the new home page template function. 86 | 87 | ```javascript 88 | new TumblrBuilder({ 89 | templatesPages: { 90 | home: customHomePage 91 | } 92 | }); 93 | ``` 94 | 95 | ## Tagged page 96 | 97 | ### Default tagged page 98 | 99 | The default template used for the tagged page is described below. The function can be replaced with the `templatesPages.tagged` parameter. The `data-infinite-scroll` attribute is used for the infinite scroll feature. 100 | 101 | ```javascript 102 | function tagged ({ templates, tag, posts }) => { 103 | /* prettier-ignore */ 104 | return ` 105 | 113 | 114 |
115 | ${posts.map(post => templates[post.type](post)).join('')} 116 |
117 | `; 118 | } 119 | ``` 120 | 121 | The function exposes following parameters: 122 | 123 | #### tagged ({ templates }) 124 | 125 | - Type: `Object` 126 | - Default: `{}` 127 | 128 | List of all template pages functions (audio, chat, link, photo, quote, text, video). All defaults functions are automatically used if `templatesPages` object is not overrides them. 129 | 130 | #### tagged ({ tag }) 131 | 132 | - Type: `String` 133 | - Default: `''` 134 | 135 | The current hashtag from the route. 136 | 137 | #### tagged ({ posts }) 138 | 139 | - Type: `Array` 140 | - Default: `[]` 141 | 142 | List of all articles filtered by the current hashtag. 143 | 144 | ### Custom tagged page 145 | 146 | To customize the tagged page template, write your own `customTaggedPage` function inspired by the `tagged` function above. 147 | 148 | ```javascript 149 | function customTaggedPage({ templates, tag, posts }) { 150 | return ` 151 | 152 | `; 153 | } 154 | ``` 155 | 156 | Next, use the `templatesPages.tagged` option to passed the new tagged page function. 157 | 158 | ```javascript 159 | new TumblrBuilder({ 160 | templatesPages: { 161 | tagged: customTaggedPage 162 | } 163 | }); 164 | ``` 165 | 166 | ## Post page 167 | 168 | ### Default post page 169 | 170 | The default template used for the post page is described below. The function can be replaced with the `templatesPages.post` parameter. 171 | 172 | ```javascript 173 | function post ({ templates, posts, relatedPosts }) => { 174 | /* prettier-ignore */ 175 | return ` 176 | 184 |
185 | ${posts.map(post => templates[post.type](post)).join('')} 186 |
187 | ${relatedPosts.length ? ` 188 | 192 | ` : ''} 193 | `; 194 | } 195 | ``` 196 | 197 | The function exposes following parameters: 198 | 199 | #### post ({ templates }) 200 | 201 | - Type: `Object` 202 | - Default: `{}` 203 | 204 | List of all template pages functions (audio, chat, link, photo, quote, text, video). All defaults functions are automatically used if `templatesPages` object is not overrides them. 205 | 206 | #### post ({ posts }) 207 | 208 | - Type: `Array` 209 | - Default: `[]` 210 | 211 | Datas for the current article. 212 | 213 | #### post ({ relatedPosts }) 214 | 215 | - Type: `Array` 216 | - Default: `[]` 217 | 218 | List of related posts for the associated article. 219 | 220 | ### Custom post page 221 | 222 | To customize the post page template, write your own `customPostPage` function inspired by the `post` function above. 223 | 224 | ```javascript 225 | function customPostPage({ templates, posts, relatedPosts }) { 226 | return ` 227 | 228 | `; 229 | } 230 | ``` 231 | 232 | Next, use the `templatesPages.post` option to passed the new post page function. 233 | 234 | ```javascript 235 | new TumblrBuilder({ 236 | templatesPages: { 237 | post: customPostPage 238 | } 239 | }); 240 | ``` 241 | 242 | ::: tip Custom related posts 243 | The `relatedPosts` parameter can be replaced by a new call to the [getRelatedPosts](available-methods.html#getrelatedposts) function with custom parameters. 244 | ::: 245 | -------------------------------------------------------------------------------- /docs/guide/custom-posts.md: -------------------------------------------------------------------------------- 1 | # Custom posts 2 | 3 | Need to customize the HTML for article types (audio, chat, link, photo, quote, text, video)? The `templatesPosts` option is for you :rocket: 4 | 5 | On app initialize, add the `templatesPosts` parameter with post types you need to customized. The others article types will automatically used their default template. 6 | 7 | ::: tip Override default templates 8 | To simply override the default template for a specific article type use the `templatesPosts.` parameter. The `` placeholder must be replaced by the following values: `audio` `chat` `link` `photo` `quote` `text` `video`. 9 | ::: 10 | 11 | ::: details Prettier ignore comment 12 | To prevent issue with Prettier and template literals indents, following examples are preceded by `/* prettier-ignore */`. [More information on Prettier](https://prettier.io/docs/en/ignore.html). 13 | ::: 14 | 15 | ## Function parameters 16 | 17 | All functions exposes following parameter. 18 | 19 | ### datas 20 | 21 | - Type: `Object` 22 | - Default: `{}` 23 | 24 | All datas available from the Tumblr API for the current article type. More informations about available fields on the [Tumblr API documentation](https://www.tumblr.com/docs/en/api/v2#posts--retrieve-published-posts). 25 | 26 | ## Audio post 27 | 28 | ### Default audio post 29 | 30 | The default template used for the audio post is described below. The function can be replaced with the `templatesPosts.audio` parameter. 31 | 32 | ```javascript 33 | function audio (datas) => { 34 | /* prettier-ignore */ 35 | return ` 36 |
37 |
38 | ${datas.player} 39 |
40 |
41 | ${datas.summary} 42 |
    43 | ${datas.tags.map(tag => ` 44 |
  • 45 | #${tag} 46 |
  • 47 | `).join('')} 48 |
49 |
50 |
51 | `; 52 | } 53 | ``` 54 | 55 | ### Custom audio post 56 | 57 | To customize the audio post template, write your own `customAudioPost` function inspired by the `audio` function above. 58 | 59 | ```javascript 60 | function customAudioPost(datas) { 61 | return ` 62 | 63 | `; 64 | } 65 | ``` 66 | 67 | Next, use the `templatesPosts.audio` parameter to passed the new audio post template function. 68 | 69 | ```javascript 70 | const tumblr = new TumblrBuilder({ 71 | templatesPosts: { 72 | audio: customAudioPost 73 | } 74 | }); 75 | ``` 76 | 77 | ## Chat post 78 | 79 | ### Default chat post 80 | 81 | The default template used for the chat post is described below. The function can be replaced with the `templatesPosts.chat` parameter. 82 | 83 | ```javascript 84 | function chat (datas) => { 85 | /* prettier-ignore */ 86 | return ` 87 |
88 |
89 | ${datas.player} 90 |
91 |
92 | ${datas.summary} 93 |
    94 | ${datas.tags.map(tag => ` 95 |
  • 96 | #${tag} 97 |
  • 98 | `).join('')} 99 |
100 |
101 |
102 | `; 103 | } 104 | ``` 105 | 106 | ### Custom chat post 107 | 108 | To customize the chat post template, write your own `customChatPost` function inspired by the `chat` function above. 109 | 110 | ```javascript 111 | function customChatPost(datas) { 112 | return ` 113 | 114 | `; 115 | } 116 | ``` 117 | 118 | Next, use the `templatesPosts.chat` parameter to passed the new chat post template function. 119 | 120 | ```javascript 121 | const tumblr = new TumblrBuilder({ 122 | templatesPosts: { 123 | chat: customChatPost 124 | } 125 | }); 126 | ``` 127 | 128 | ## Link post 129 | 130 | ### Default link post 131 | 132 | The default template used for the link post is described below. The function can be replaced with the `templatesPosts.link` parameter. 133 | 134 | ```javascript 135 | function link (datas) => { 136 | /* prettier-ignore */ 137 | return ` 138 | 151 | `; 152 | } 153 | ``` 154 | 155 | ### Custom link post 156 | 157 | To customize the link post template, write your own `customLinkPost` function inspired by the `link` function above. 158 | 159 | ```javascript 160 | function customLinkPost(datas) { 161 | return ` 162 | 163 | `; 164 | } 165 | ``` 166 | 167 | Next, use the `templatesPosts.link` parameter to passed the new link post template function. 168 | 169 | ```javascript 170 | const tumblr = new TumblrBuilder({ 171 | templatesPosts: { 172 | link: customLinkPost 173 | } 174 | }); 175 | ``` 176 | 177 | ## Photo post 178 | 179 | ### Default photo post 180 | 181 | The default template used for the photo post is described below. The function can be replaced with the `templatesPosts.photo` parameter. 182 | 183 | ```javascript 184 | function photo (datas) => { 185 | const isPhotoset = datas.photos.length > 1 186 | /* prettier-ignore */ 187 | return ` 188 |
189 |
190 |
    191 | ${datas.photos.map(photo => ` 192 |
  • 193 | ${datas.summary} 194 |
  • 195 | `).join('')} 196 |
197 | ${datas.summary 198 | ? `${datas.summary}` 199 | : ''} 200 |
    201 | ${datas.tags.map(tag => ` 202 |
  • 203 | #${tag} 204 |
  • 205 | `).join('')} 206 |
207 |
208 |
209 | `; 210 | } 211 | ``` 212 | 213 | ### Custom photo post 214 | 215 | To customize the photo post template, write your own `customPhotoPost` function inspired by the `photo` function above. 216 | 217 | ```javascript 218 | function customPhotoPost(datas) { 219 | return ` 220 | 221 | `; 222 | } 223 | ``` 224 | 225 | Next, use the `templatesPosts.photo` parameter to passed the new photo post template function. 226 | 227 | ```javascript 228 | const tumblr = new TumblrBuilder({ 229 | templatesPosts: { 230 | photo: customPhotoPost 231 | } 232 | }); 233 | ``` 234 | 235 | ## Quote post 236 | 237 | ### Default quote post 238 | 239 | The default template used for the quote post is described below. The function can be replaced with the `templatesPosts.quote` parameter. 240 | 241 | ```javascript 242 | function quote (datas) => { 243 | /* prettier-ignore */ 244 | return ` 245 |
246 |
247 |
248 |

${datas.text}

249 |
${datas.source}
250 |
251 |
    252 | ${datas.tags.map(tag => ` 253 |
  • 254 | #${tag} 255 |
  • 256 | `).join('')} 257 |
258 |
259 |
260 | `; 261 | } 262 | ``` 263 | 264 | ### Custom quote post 265 | 266 | To customize the quote post template, write your own `customQuotePost` function inspired by the `quote` function above. 267 | 268 | ```javascript 269 | function customQuotePost(datas) { 270 | return ` 271 | 272 | `; 273 | } 274 | ``` 275 | 276 | Next, use the `templatesPosts.quote` parameter to passed the new quote post parameter function. 277 | 278 | ```javascript 279 | const tumblr = new TumblrBuilder({ 280 | templatesPosts: { 281 | quote: customQuotePost 282 | } 283 | }); 284 | ``` 285 | 286 | ## Text post 287 | 288 | ### Default text post 289 | 290 | The default template used for the text post is described below. The function can be replaced with the `templatesPosts.text` parameter. 291 | 292 | ```javascript 293 | function text (datas) => { 294 | /* prettier-ignore */ 295 | return ` 296 |
297 |
298 | ${datas.title} 299 | ${datas.body} 300 |
    301 | ${datas.tags.map(tag => ` 302 |
  • 303 | #${tag} 304 |
  • 305 | `).join('')} 306 |
307 |
308 |
309 | `; 310 | } 311 | ``` 312 | 313 | ### Custom text post 314 | 315 | To customize the text post template, write your own `customTextPost` function inspired by the `text` function above. 316 | 317 | ```javascript 318 | function customTextPost(datas) { 319 | return ` 320 | 321 | `; 322 | } 323 | ``` 324 | 325 | Next, use the `templatesPosts.text` parameter to passed the new text post template function. 326 | 327 | ```javascript 328 | const tumblr = new TumblrBuilder({ 329 | templatesPosts: { 330 | text: customTextPost 331 | } 332 | }); 333 | ``` 334 | 335 | ## Video post 336 | 337 | ### Default video post 338 | 339 | The default template used for the video post is described below. The function can be replaced with the `templatesPosts.video` parameter. 340 | 341 | ```javascript 342 | function video (datas) => { 343 | /* prettier-ignore */ 344 | return ` 345 |
346 |
347 | ${datas.player[2].embed_code} 348 |
349 |
350 | ${datas.summary} 351 |
    352 | ${datas.tags.map(tag => ` 353 |
  • 354 | #${tag} 355 |
  • 356 | `).join('')} 357 |
358 |
359 |
360 | `; 361 | } 362 | ``` 363 | 364 | ### Custom video post 365 | 366 | To customize the video post template, write your own `customVideoPost` function inspired by the `video` function above. 367 | 368 | ```javascript 369 | function customVideoPost(datas) { 370 | return ` 371 | 372 | `; 373 | } 374 | ``` 375 | 376 | Next, use the `templatesPosts.video` parameter to passed the new video post template function. 377 | 378 | ```javascript 379 | const tumblr = new TumblrBuilder({ 380 | templatesPosts: { 381 | video: customVideoPost 382 | } 383 | }); 384 | ``` 385 | -------------------------------------------------------------------------------- /src/scripts/tumblr-builder.js: -------------------------------------------------------------------------------- 1 | import { getScrollTop, getRandoms } from './utils' 2 | 3 | import './styles' 4 | 5 | /*! 6 | * TumblrBuilder v2.0.0 7 | * (c) 2013-2020 Yoriiis 8 | * Released under the MIT License. 9 | */ 10 | export default class TumblrBuilder { 11 | /** 12 | * @param {options} 13 | */ 14 | constructor (options) { 15 | const userOptions = options || {} 16 | const defaultOptions = { 17 | element: null, 18 | host: '', 19 | apiKey: '', 20 | limitData: 250, 21 | cache: false, 22 | cacheMethod: 'sessionStorage', 23 | nearBottom: 350, 24 | elementsPerPage: 20, 25 | templatesPages: {}, 26 | templatesPosts: {} 27 | } 28 | 29 | // Merge default options with user options 30 | this.options = Object.assign(defaultOptions, userOptions) 31 | 32 | // No configurables params 33 | this.browserStorageKey = 'TumblrBuilderJsonData' 34 | this.endPage = false 35 | this.isLoading = false 36 | this.jsonComplete = false 37 | this.currentPage = 1 38 | this.nbPostPerRequest = 50 39 | this.datas = {} 40 | 41 | this.keysTemplatesPage = ['home', 'post', 'tagged'] 42 | this.keysTemplatePosts = ['audio', 'chat', 'link', 'photo', 'quote', 'text', 'video'] 43 | 44 | this.onScroll = this.onScroll.bind(this) 45 | this.hashChanged = this.hashChanged.bind(this) 46 | } 47 | 48 | /** 49 | * Function to initialize the app 50 | */ 51 | init = async () => { 52 | // Get JSON and push it in cache if option is active and if it's possible 53 | this.datas = await this.getAllDatas() 54 | 55 | if (this.datas.posts.length === 0) { 56 | window[this.options.cacheMethod].removeItem('TumblrBuilderJsonData') 57 | return false 58 | } 59 | this.jsonComplete = true 60 | this.datas.tags = await this.getAllTags() 61 | this.totalPages = Math.ceil(this.datas.totalPosts / this.options.elementsPerPage) 62 | 63 | this.addEvents() 64 | this.templates = await this.getTemplates() 65 | 66 | // Get current route 67 | this.currentRoute = this.getRoute() 68 | 69 | this.hashChanged() 70 | 71 | return this.datas 72 | } 73 | 74 | /** 75 | * Create event listeners 76 | */ 77 | addEvents () { 78 | window.addEventListener('scroll', this.onScroll, false) 79 | window.addEventListener('hashchange', this.hashChanged, false) 80 | } 81 | 82 | /** 83 | * On hash changed event listener 84 | * 85 | * @param {Object} e Event listener datas 86 | */ 87 | hashChanged = async e => { 88 | const currentTag = this.getHashTagFromRoute() 89 | const currentPostId = this.getPostIdFromRoute() 90 | const pageType = this.getPageType() 91 | let posts 92 | 93 | if (pageType === 'tagged' && currentTag) { 94 | posts = await this.getDatasForTaggedPage(currentTag) 95 | this.buildPage( 96 | this.templates.pages.tagged({ 97 | templates: this.templates.posts, 98 | tag: currentTag, 99 | posts 100 | }) 101 | ) 102 | } else if (pageType === 'post' && currentPostId) { 103 | posts = await this.getDatasForPostPage(currentPostId) 104 | // Back to home page if post page returns more than one article 105 | if (posts.length > 1) { 106 | this.setRoute('') 107 | return 108 | } 109 | const relatedPosts = this.getRelatedPosts({ 110 | postId: currentPostId, 111 | tags: posts[0].tags, 112 | limit: 3 113 | }) 114 | this.buildPage( 115 | this.templates.pages.post({ 116 | templates: this.templates.posts, 117 | posts, 118 | relatedPosts 119 | }) 120 | ) 121 | } else if (pageType === 'home') { 122 | posts = await this.getDatasForHomePage() 123 | this.buildPage( 124 | this.templates.pages.home({ 125 | templates: this.templates.posts, 126 | tags: this.datas.tags, 127 | posts 128 | }) 129 | ) 130 | } 131 | 132 | // Reset class properties on page changes 133 | this.endPage = false 134 | this.currentPage = 1 135 | window.scrollTo(0, 0) 136 | } 137 | 138 | /** 139 | * On scroll event listener 140 | * 141 | * @param {Object} e Event listener datas 142 | */ 143 | onScroll = async e => { 144 | const page = this.getPageType() 145 | if (!this.isLoading && !this.endPage && (page === 'home' || page === 'tagged')) { 146 | if ( 147 | getScrollTop() >= 148 | document.body.clientHeight - window.innerHeight - this.options.nearBottom 149 | ) { 150 | this.isLoading = true 151 | if (this.currentPage >= this.totalPages) { 152 | this.endPage = true 153 | } else { 154 | await this.loadNewPage() 155 | this.currentPage++ 156 | } 157 | this.isLoading = false 158 | } 159 | } 160 | } 161 | 162 | /** 163 | * Build the page 164 | * 165 | * @param {Array} posts List of posts for the current page 166 | * @param {Array} relatedPosts List of related posts if available 167 | */ 168 | buildPage (html) { 169 | this.options.element.innerHTML = html 170 | } 171 | 172 | /** 173 | * Load new page on infinite scroll 174 | * Get next posts from the API 175 | * Inject new content after current posts on the [data-infinite-scroll] element 176 | */ 177 | loadNewPage = async () => { 178 | const posts = await this.getPostsByPageNumber(this.currentPage + 1) 179 | const infiniteScrollElement = this.options.element.querySelector('[data-infinite-scroll]') 180 | 181 | if (infiniteScrollElement !== null) { 182 | infiniteScrollElement.insertAdjacentHTML( 183 | 'beforeend', 184 | `${posts.map(post => this.templates.posts[post.type](post)).join('')}` 185 | ) 186 | } 187 | } 188 | 189 | /** 190 | * Set the route 191 | * 192 | * @param {String} route New value for the route 193 | */ 194 | setRoute (route) { 195 | window.location.hash = route 196 | } 197 | 198 | /** 199 | * Request datas from the API 200 | * 201 | * @param {String} url API url 202 | * 203 | * @returns {Promise} Promise results from the API 204 | */ 205 | requestAPI (url) { 206 | return fetch(url).then(response => response.json()) 207 | } 208 | 209 | /** 210 | * Check if the Response from the API is valid 211 | * 212 | * @param {Object} datas Response datas 213 | * 214 | * @returns {Boolean} Is a valid Response 215 | */ 216 | isValidResponse (datas) { 217 | return datas.meta.status === 200 218 | } 219 | 220 | /** 221 | * Extract a range of posts from browser storage if available 222 | * 223 | * @param {Object} range Start and end index for the range 224 | * 225 | * @returns {Array} List of corresponding posts 226 | */ 227 | extractDatasFromLocalDatas (range) { 228 | return this.datas.posts.slice(range.start, range.end + 1) 229 | } 230 | 231 | /** 232 | * Get templates from constructor options templatesPages and templatesPosts 233 | * Else, dynamic import default template for each types 234 | * 235 | * @returns {Object} List of template functions by type 236 | */ 237 | getTemplates = async () => { 238 | return { 239 | pages: await this.getTemplatesByType({ 240 | keys: this.keysTemplatesPage, 241 | path: './templates/pages/', 242 | custom: this.options.templatesPages 243 | }), 244 | posts: await this.getTemplatesByType({ 245 | keys: this.keysTemplatePosts, 246 | path: './templates/posts/', 247 | custom: this.options.templatesPosts 248 | }) 249 | } 250 | } 251 | 252 | /** 253 | * Get templates between default and custom templates 254 | * Priority to custom templates 255 | * 256 | * @param {Object} 257 | * @param {String} keys Keys for template pages (home, tagged, post) 258 | * @param {String} path Path to dynamic import default templates 259 | * @param {String} custom Custom templates for the current type 260 | * 261 | * @returns {Object} List of templates for the current type (pages, posts) 262 | */ 263 | getTemplatesByType = async ({ keys, path, custom }) => { 264 | const templates = {} 265 | 266 | const customTemplates = keys.filter(name => this.hasProperty(custom, name)) 267 | customTemplates.forEach(name => { 268 | templates[name] = custom[name] 269 | }) 270 | const defaultTemplates = keys.filter(name => !this.hasProperty(custom, name)) 271 | 272 | const requestsDefaultTemplates = defaultTemplates.map(name => import(path + name)) 273 | 274 | const responses = await Promise.all(requestsDefaultTemplates) 275 | 276 | responses.forEach((response, index) => { 277 | templates[defaultTemplates[index]] = response.default 278 | }) 279 | 280 | return templates 281 | } 282 | 283 | /** 284 | * Build the Tumblr API url 285 | * 286 | * @param {Object} 287 | * @param {(String|Boolean)} id Post id 288 | * @param {Integer} offset Offset for the query 289 | * @param {Integer} limit Limit of results for the query 290 | * @param {String} tag Filter by a unique post tag 291 | * 292 | * @returns {String} API url for the request 293 | */ 294 | getAPIUrl ({ 295 | id = false, 296 | offset = 0, 297 | limit = this.options.limitData < this.nbPostPerRequest 298 | ? this.options.limitData 299 | : this.nbPostPerRequest, 300 | tag = false 301 | } = {}) { 302 | return `//api.tumblr.com/v2/blog/${this.options.host}/posts/?api_key=${ 303 | this.options.apiKey 304 | }&limit=${limit}¬es_info=false&offset=${offset}${tag ? `&tag=${tag}` : ''}${ 305 | id ? `&id=${id}` : '' 306 | }` 307 | } 308 | 309 | /** 310 | * Get all datas from the API 311 | * Store datas in browser storage is option is enabled 312 | * 313 | * @returns {Object} Datas from the API 314 | */ 315 | getAllDatas = async e => { 316 | let datas 317 | 318 | if (this.options.cache) { 319 | var getDataFromCache = window[this.options.cacheMethod].getItem(this.browserStorageKey) 320 | 321 | if (getDataFromCache === null) { 322 | datas = await this.getDataFromAPI() 323 | 324 | window[this.options.cacheMethod].setItem( 325 | this.browserStorageKey, 326 | JSON.stringify(datas) 327 | ) 328 | } else { 329 | datas = JSON.parse(window[this.options.cacheMethod].getItem(this.browserStorageKey)) 330 | } 331 | } else { 332 | datas = await this.getDataFromAPI() 333 | window[this.options.cacheMethod].removeItem(this.browserStorageKey) 334 | } 335 | 336 | return datas 337 | } 338 | 339 | /** 340 | * Get datas from the API 341 | * 342 | * @returns {Object} Datas from the API 343 | */ 344 | getDataFromAPI = async () => { 345 | // Trigger the first request to get infos about totalPosts and calculate next requests 346 | const datasFirstRequest = await this.requestAPI(this.getAPIUrl()) 347 | 348 | if (this.isValidResponse(datasFirstRequest)) { 349 | const totalPosts = datasFirstRequest.response.total_posts 350 | const nbLoop = this.getNumberOfRequiredRequests(totalPosts) 351 | let posts = datasFirstRequest.response.posts 352 | 353 | if (datasFirstRequest.response.posts.length && nbLoop) { 354 | const requests = [] 355 | 356 | // Do multiple loop according to nbLoop and options.limitData 357 | for (var i = 0; i < nbLoop; i++) { 358 | requests.push( 359 | this.requestAPI( 360 | this.getAPIUrl({ 361 | offset: this.nbPostPerRequest + this.nbPostPerRequest * i 362 | }) 363 | ) 364 | ) 365 | } 366 | 367 | await Promise.all(requests).then(responses => { 368 | responses.forEach(response => { 369 | posts = posts.concat(response.response.posts) 370 | }) 371 | }) 372 | } 373 | return { totalPosts, posts } 374 | } else { 375 | // If the request failed, return empty results to prevent break the application 376 | return { totalPosts: 0, posts: [] } 377 | } 378 | } 379 | 380 | /** 381 | * Get number of required requests to reach options.limitDatas 382 | * without the first request datas 383 | * 384 | * @param {*} totalPosts Total of posts from the API 385 | * 386 | * @returns {Integer} Number of request to reach options.limitDatas 387 | */ 388 | getNumberOfRequiredRequests (totalPosts) { 389 | return totalPosts <= this.options.limitData 390 | ? Math.ceil((totalPosts - this.nbPostPerRequest) / this.nbPostPerRequest) 391 | : Math.ceil((this.options.limitData - this.nbPostPerRequest) / this.nbPostPerRequest) 392 | } 393 | 394 | /** 395 | * Get datas for the homepage (first page only) 396 | * Datas are available in the class property 397 | * 398 | * @returns {Object} Posts datas for the homepage 399 | */ 400 | getDatasForHomePage () { 401 | const maxPosts = 402 | this.datas.totalPosts < this.options.elementsPerPage 403 | ? this.datas.totalPosts 404 | : this.options.elementsPerPage 405 | 406 | return this.datas.posts.slice(0, maxPosts) 407 | } 408 | 409 | /** 410 | * Get datas for the tagged page 411 | * Request the API to get posts 412 | * 413 | * @param {String} tag Tag to filter posts 414 | * 415 | * @returns {Object} Posts datas for the tagged page filter by tag 416 | */ 417 | getDatasForTaggedPage = async tag => { 418 | const datas = await this.requestAPI( 419 | this.getAPIUrl({ 420 | tag, 421 | limit: this.options.elementsPerPage 422 | }) 423 | ) 424 | 425 | return datas && datas.response ? datas.response.posts : [] 426 | } 427 | 428 | /** 429 | * Get datas for the post page 430 | * Request the API to get posts 431 | * 432 | * @param {String} id Id of the current post 433 | * 434 | * @returns {Object} Posts datas for the post page filter by post id 435 | */ 436 | getDatasForPostPage = async id => { 437 | const datas = await this.requestAPI( 438 | this.getAPIUrl({ 439 | id: id 440 | }) 441 | ) 442 | 443 | return datas && datas.response ? datas.response.posts : [] 444 | } 445 | 446 | /** 447 | * Get datas by page number, use by the infinite scroll 448 | * Request the local datas in priority 449 | * If no datas are available, request the API to get posts 450 | * 451 | * @param {Integer} pageNumber Page number 452 | * 453 | * @returns {Object} Posts datas for the homepage filter by page number 454 | */ 455 | getPostsByPageNumber = async pageNumber => { 456 | const range = this.getRange(pageNumber) 457 | const datas = this.extractDatasFromLocalDatas(range) 458 | 459 | // Only home page can used local datas, tagged page need to filter (request directly) 460 | if (datas.length && this.getPageType() === 'home') { 461 | return datas 462 | } else { 463 | const datas = await this.requestAPI( 464 | this.getAPIUrl({ 465 | offset: range.start, 466 | limit: this.options.elementsPerPage, 467 | tag: this.getPageType() === 'tagged' ? this.getHashTagFromRoute() : false 468 | }) 469 | ) 470 | return datas.response.posts 471 | } 472 | } 473 | 474 | /** 475 | * Get the type of the current page according to the hash 476 | * 477 | * @returns {(String|null)} Page type (tagged, post, home) 478 | */ 479 | getPageType () { 480 | const hash = this.getRoute() 481 | const defaultRoute = ['', '#', '#_'] 482 | 483 | if (hash.indexOf('/tagged/') !== -1) { 484 | return 'tagged' 485 | } else if (hash.indexOf('/post/') !== -1) { 486 | return 'post' 487 | } else if (defaultRoute.includes(hash)) { 488 | return 'home' 489 | } else { 490 | return null 491 | } 492 | } 493 | 494 | /** 495 | * Get range index (start, end) to extract posts from local datas 496 | * according to the current page position and elements per page 497 | * 498 | * @param {Integer} pageNumber Pagz number 499 | * 500 | * @returns {Object} Start and end index for the range 501 | */ 502 | getRange (pageNumber) { 503 | const previousPage = pageNumber > 1 ? pageNumber - 1 : pageNumber 504 | return { 505 | start: previousPage * this.options.elementsPerPage, 506 | end: pageNumber * this.options.elementsPerPage - 1 507 | } 508 | } 509 | 510 | /** 511 | * Get hashtag from the route 512 | * 513 | * @returns {String} Current tag 514 | */ 515 | getHashTagFromRoute () { 516 | return this.getRoute().split('/tagged/')[1] 517 | } 518 | 519 | /** 520 | * Get post id from the route 521 | * 522 | * @returns {String} Current post id 523 | */ 524 | getPostIdFromRoute () { 525 | return this.getRoute().split('/post/')[1] 526 | } 527 | 528 | /** 529 | * Get the current route (hash) 530 | * 531 | * @returns {String} Current hash 532 | */ 533 | getRoute () { 534 | return window.location.hash.substr(1) 535 | } 536 | 537 | /** 538 | * Get related posts 539 | * Research is executed is local datas collection only 540 | * 541 | * @param {Object} 542 | * @param {String} postId Pot id 543 | * @param {Integer} limit Limit of related posts results 544 | * @param {Array} tags Tags list of the current post 545 | * @param {Array} ignoreTags Ignore specific list of tags 546 | * 547 | * @returns {Array} List of related posts associated to the current post 548 | */ 549 | getRelatedPosts ({ postId, limit = 3, tags = [], ignoreTags = [] }) { 550 | // Get all tags from current post without ignore tags 551 | const tagsSource = tags.filter(tag => !ignoreTags.includes(tag)) 552 | 553 | // Get related posts 554 | const relatedPosts = this.datas.posts.filter(post => 555 | post.tags.some(tag => tagsSource.includes(tag) && post.id_string !== postId) 556 | ) 557 | 558 | // Get random keys 559 | const randomKeys = getRandoms(limit, 0, relatedPosts.length - 1) || [] 560 | 561 | if (relatedPosts.length < limit) { 562 | return relatedPosts 563 | } else { 564 | // Extract random related posts 565 | return randomKeys.map(key => relatedPosts[key]) 566 | } 567 | } 568 | 569 | /** 570 | * Get all tags from local datas 571 | * Tags are sorted alphabetical 572 | * Tags are search only from local datas according to options.limitData posts 573 | * 574 | * @returns {Array} List of tags 575 | */ 576 | getAllTags () { 577 | return this.jsonComplete 578 | ? this.datas.posts 579 | .filter(post => post.tags.length) 580 | .flatMap(post => post.tags) 581 | .map(tag => tag.toLowerCase()) 582 | .filter((elem, pos, arr) => { 583 | return arr.indexOf(elem) === pos 584 | }) 585 | .sort() 586 | : [] 587 | } 588 | 589 | /** 590 | * Custom hasOwnProperty to prevent security issue (ESLint no-prototype-builtins) 591 | * 592 | * @param {Object} objectSource Object source to search the key 593 | * @param {String} key The key to search inside the object 594 | * 595 | * @returns {Boolean} Is the key present inside the object 596 | */ 597 | hasProperty (objectSource, key) { 598 | return Object.prototype.hasOwnProperty.call(objectSource, key) 599 | } 600 | } 601 | --------------------------------------------------------------------------------