├── .gitignore ├── 404.html ├── GUIDE.md ├── LICENSE ├── README.md ├── app ├── app.js ├── components │ ├── blocks │ │ ├── article.js │ │ └── category.js │ ├── disqus │ │ ├── disqusCount.js │ │ └── disqusThread.js │ ├── form │ │ └── baseInput.js │ └── layout │ │ ├── footer.js │ │ ├── menu.js │ │ ├── menuBurger.js │ │ ├── page.js │ │ ├── postsAndCategories.js │ │ └── sidebar.js ├── hooks │ ├── useActivePanel.js │ ├── useArticleText.js │ ├── useCategoriesAndArticles.js │ ├── useMenuVisible.js │ └── usePageMeta.js ├── lib │ ├── api.js │ ├── drive.js │ ├── htm-preact.js │ └── mail.js ├── package.json ├── routes │ ├── about.js │ ├── article.js │ ├── category.js │ ├── contact.js │ └── home.js ├── state.js ├── styles │ ├── Spinner.js │ ├── blocks.js │ ├── buttons.js │ └── input.js └── utils │ ├── avoidReload.js │ ├── capitalize.js │ ├── debounce.js │ ├── jsonpCall.js │ ├── path.js │ ├── prefixUriIfNeeded.js │ ├── sleep.js │ ├── to.js │ └── uuid.js ├── assets ├── default-about.jpg ├── default-contact.jpg ├── default-sidebar.jpg ├── favicon.ico ├── profile-1.jpg └── react_logo.png ├── conf.js ├── dev ├── 404.html ├── conf.js ├── index.html └── server.js ├── index.html ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | ### How to Setup Google Drive to use React Drive CMS 2 | 3 | Some users were having trouble setting up their Google Drive folder properly for their website to work, so I've decided to make this detailed step-by-step guide: 4 | 5 | ##### 1. In your Google Drive, create a new folder named "DriveCMS". Right-click on it and select "Share" and "Public with Link". All the contents of the folder can now be accessed by anonymous users. 6 | 7 | ##### 2. Double click on DriveCMS to open it. In DriveCMS folder: 8 | - create a new Google Sheets spreadsheet named "Dashboard" and select "create and share" when prompted. 9 | - Also create spreadsheet "Visitors" and select "create and share". 10 | - create folder "Posts" 11 | - create folder "Images" 12 | 13 | ##### 3. Double click on "Posts" to open the Posts folder. In the "Posts" folder: 14 | - click on "New" in the upper left corner and select "Google Docs" to create a new Google Docs document in the "Posts" folder. Select "create and share" when prompted. 15 | - create a couple Google Docs documents, give them a title and some content. For testing purposes you can copy some wikipedia articles. 16 | 17 | ##### 4. Add a couple images in the "Images" folder that you created in step 2. 18 | 19 | ##### 5. Open the "Dashboard" spreadsheet you created in step 2. 20 | - Click on "Tools", then on "Script Editor". A new tab wil open with the Google Script Editor. 21 | - Rename "Untitled Project" to DriveCMS. 22 | - Copy the contents of https://gist.githubusercontent.com/misterfresh/e1c9cf0bb4c777221f84/raw/50c51d82fd1e73e35d64e6c3ce0fdd5a9ac1c7e8/prepareSheets.js and paste it into Code.gs in the Script Editor. 23 | - Click on "File" and "Save". 24 | - Go back to the "Dashboard" spreadsheet and force reload it. You should see a new option appear in the top-right of the menu, named "Update". 25 | - Click on "Update", then on "Update dashboard". When prompted, give full authorization to the bound script. When prompted that the application is not valid, click on "advanced parameters" and select "access DriveCMS (not secure)". When Prompted a third time, scroll down and select "Authorize". After granting all permissions, you will probably need to click one more time on "Update", then "Update dashboard" to finally run the script, and see the spreadsheet updated. (You can also run the script from the Script Editor, by selecting "run", "run function", "prepareSheets"). 26 | 27 | ##### 6. The "Dashboard" spreadsheet should now be pre-filled, open the "Posts" sheet by selecting it in the bottom left tab: 28 | - In the Title column you should see the titles of the posts you created in the "Posts" directory. 29 | - You can manually type in a subtitle for your posts in the Subtitle column. 30 | - Define categories in the Category column. If you give two posts the same category, they will be grouped together in the app. 31 | - Select a main image for an article from the dropdown list in the "Image" column. This should automatically update the "Image Id" column with the corresponding ID. You can find all available image IDs in the "Images" sheet (select it by clicking the tab in the bottom left). 32 | - You should not need to manually edit the "Images" sheet. After adding new images to the Images folder, you can re-run the Update/Update Dashboard script to make them available. 33 | - Click on "File", then click on "Publish on the web". You can leave the default settings "Full Document" and "web page". Click the green "Publish" button. This will make the "Dashboard" public. 34 | 35 | ##### 7. Create a new project and related API key in Google Cloud Console 36 | From September 2021, the Google Spreadsheet API v3 was discontinued. In order to use v4 of the API, you need to create a project in the Google Cloud Console, then create an API key linked to that project, with access to your Google Drive and Spreadsheet. Also you should restrict the API key to work only on your domain. You need to set that API key in /app/lib/drive.js file. 37 | 38 | ##### 8. Now let's make sure it works. Fork this repo, then clone it to your computer. Open the project in a text editor. 39 | - in the "dev" folder of the project, open "conf.js". You need to change the "dashboardId" parameter and set it to the ID of your "Dashboard" spreadsheet. You can find that ID by opening the "Dashboard" spreadsheet and looking at the URL in the address bar. The ID is the string between "https://docs.google.com/spreadsheets/d/" and "/edit#gid=0" 40 | - run command "npm run start". Open your browser to http://localhost:8080/. You should see the list of your Posts and Categories displayed in the right pane. 41 | 42 | ##### 9. To remove a Post you can delete it from the "Posts" folder, then re-run the Update/Update Dashboard script. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Antoine S 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReactDrive CMS v4.2.7, updated for 2022! 2 | Publish articles directly from Google Drive to your blog with React JS. Serverless setup with no transpiling. 3 | In 2022, the setup has a couple extra steps, mostly because of changes with the third party services: 4 | - Google Spreadsheets API v3 has been discontinued and the update to the v4 requires the use of an API key that you need to configure in the Google Cloud Console 5 | - Sendgrid requires to create a verified sender account 6 | 7 | Live demo here: 8 | http://misterfresh.github.io/react-drive-cms/ 9 | 10 | ### Features: 11 | - A dynamic site, but no backend to manage, no database, no server, no hosting & no maintenance (almost) 12 | - A MS Word-like and MS Excel-like interface that users are familiar with 13 | - Disqus comments system integration with React Hooks 14 | - SendGrid email forwarding integration so that you can receive messages from the contact form on your email address. 15 | - Regular URLs (no hashbang) 16 | - A simple blog starter kit that can be easily customized to your liking. 17 | 18 | ### How to use: 19 | 20 | 1) Connecting your React App to your Google Drive : follow this detailed step-by-step guide: https://github.com/misterfresh/react-drive-cms/blob/master/GUIDE.md 21 | 22 | 2) In order for the contact form to work, publish the following script as a Google Web App: 23 | https://gist.github.com/misterfresh/b69d29a97cf415980be2 . In your "Visitors" Google spreadsheet, go to Tools>Script Editor. You can create a free SendGrid account and paste your SendGrid API key in the script. You will also need to have a verified sender email with SendGrid, and set that email in the "From" and "ReplyTo" fields in the script on lines 55 and 59. Save the script and click on Publish>Deploy as Web App. 24 | 25 | 3) Fill in the correct values in ./conf.js : 26 | - "dashboardId" is the id of the "Dashboard" Google spreadsheet. 27 | - "sendContactMessageUrlId" is the id of the Google Web App script that does the email forwarding 28 | - "shortname" is the website's Disqus identifier 29 | - "root" is an optional url parameter, that would be the name of the project on GitHub pages. 30 | 31 | 4) Push these changes to your forked repository on GitHub, go to your repo's Settings tab to publish it. 32 | 33 | That's it! 34 | 35 | ### 7 years of ReactDrive CMS! 36 | ReactDrive CMS was started in October 2015. The objective was to provide a free, most hassle-free way of hosting a basic dynamic website. 37 | 38 | It uses Github Pages to host static assets, and uses React to generate the HTML on the client. The CMS part is handled using google Docs, and a google Spreadsheet is the "database". The contact form is the most complicated part : the form data will be sent to a public google apps script which will in turn write the visitor in the logbook spreadsheet and then post the form data to the sendgrid api, which will send an email to the site owner. 39 | 40 | This setup has proven remarkably stable over the years, requiring only minor changes every 3 years or so to stay online, and having zero operating costs. 41 | 42 | It has evolved with React patterns, starting with Browserify and Gulp and then later using Redux, and now switching to React Hooks and using Preact and htm library under the hood to avoid transpiling the code. 43 | 44 | ### How to customize, no tools required: 45 | From a terminal, run: 46 | ```` 47 | npm run start 48 | ```` 49 | This will start the local server. Open http://localhost:8080/ in your favorite browser. 50 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import { 2 | html, 3 | render, 4 | useReducer, 5 | useMemo, 6 | useEffect, 7 | } from './lib/htm-preact.js' 8 | import debounce from './utils/debounce.js' 9 | import { reducer, initialState } from './state.js' 10 | 11 | import { Article } from './routes/article.js' 12 | import { Category } from './routes/category.js' 13 | 14 | import { About } from './routes/about.js' 15 | import { Contact } from './routes/contact.js' 16 | import { Home } from './routes/home.js' 17 | import { getActiveItemId, getPageName } from './utils/path.js' 18 | 19 | const App = () => { 20 | const [state, dispatch] = useReducer(reducer, initialState) 21 | 22 | const pageName = state?.pageName 23 | const CurrentPage = useMemo(() => { 24 | let Page = Home 25 | if (pageName === 'about') { 26 | Page = About 27 | } else if (pageName === 'contact') { 28 | Page = Contact 29 | } else if (pageName === 'categories') { 30 | Page = Category 31 | } else if (pageName === 'articles') { 32 | Page = Article 33 | } 34 | return Page 35 | }, [pageName]) 36 | 37 | useEffect(() => { 38 | const showState = () => { 39 | debouncedConsole(state) 40 | } 41 | window.addEventListener('showState', showState) 42 | return function cleanup() { 43 | window.removeEventListener('showState', showState) 44 | } 45 | }, [state]) 46 | 47 | useEffect(() => { 48 | const updatePath = () => { 49 | dispatch({ 50 | type: 'URI_CHANGE', 51 | pageName: getPageName(), 52 | activeItemId: getActiveItemId(), 53 | }) 54 | } 55 | window.addEventListener('popstate', updatePath) 56 | return function cleanup() { 57 | window.removeEventListener('popstate', updatePath) 58 | } 59 | }, [dispatch]) 60 | 61 | return html`<${CurrentPage} state=${state} dispatch=${dispatch} />` 62 | } 63 | 64 | render(html`<${App} />`, document.getElementById('app-mount')) 65 | 66 | Object.defineProperty(window, 'state', { 67 | async get() { 68 | dispatchEvent(new CustomEvent('showState')) 69 | }, 70 | configurable: true, 71 | enumerable: true, 72 | }) 73 | const debouncedConsole = debounce(console.log, 1000) 74 | -------------------------------------------------------------------------------- /app/components/blocks/article.js: -------------------------------------------------------------------------------- 1 | import { html } from '../../lib/htm-preact.js' 2 | import { avoidReload } from '../../utils/avoidReload.js' 3 | import prefixUriIfNeeded from '../../utils/prefixUriIfNeeded.js' 4 | 5 | export const Article = ({ article, category }) => html` 6 | 73 | 74 |
75 |

76 | 82 | ${article.title} 83 | 84 |

85 |

${article.subtitle}

86 |

87 | 95 | - Published in : 96 | 102 | ${category.title} 103 | 104 |

105 |
106 | ` 107 | -------------------------------------------------------------------------------- /app/components/blocks/category.js: -------------------------------------------------------------------------------- 1 | import { html } from '../../lib/htm-preact.js' 2 | import { avoidReload } from '../../utils/avoidReload.js' 3 | import prefixUriIfNeeded from '../../utils/prefixUriIfNeeded.js' 4 | 5 | export const Category = ({ category, setActivePanel }) => html` 6 | 47 | 48 | { 55 | avoidReload(event) 56 | setActivePanel('posts') 57 | }} 58 | > 59 |

${category.title}

60 |
61 | ` 62 | -------------------------------------------------------------------------------- /app/components/disqus/disqusCount.js: -------------------------------------------------------------------------------- 1 | import { html, useEffect, useState } from '../../lib/htm-preact.js' 2 | import { to } from '../../utils/to.js' 3 | 4 | const conf = window.appConf 5 | const DISQUS_COUNT_URL = `https://${conf.shortname}.disqus.com/count.js` 6 | 7 | export const DisqusCount = ({ categories }) => { 8 | const [disqusCountScript, setDisqusCountScript] = useState(null) 9 | 10 | const loadDisqusCountScript = async () => { 11 | const [loadError, loadedScript] = await to( 12 | new Promise(function (resolve, reject) { 13 | const head = document.getElementsByTagName('head')[0] 14 | const script = document.createElement('script') 15 | 16 | script.type = 'text/javascript' 17 | script.addEventListener('load', function (scriptData) { 18 | resolve(scriptData) 19 | }) 20 | script.defer = true 21 | script.className = 'disqus-count-script' 22 | script.src = DISQUS_COUNT_URL 23 | head.appendChild(script) 24 | }) 25 | ) 26 | if (loadError) { 27 | console.log('failed loading disqus count script', loadError) 28 | } else { 29 | setDisqusCountScript(loadedScript) 30 | } 31 | } 32 | const removeDisqusCountScript = () => { 33 | if (disqusCountScript && disqusCountScript.parentNode) { 34 | disqusCountScript.parentNode.removeChild(disqusCountScript) 35 | setDisqusCountScript(null) 36 | } 37 | } 38 | 39 | useEffect(async () => { 40 | window.disqus_shortname = conf.shortname 41 | if (typeof window.DISQUSWIDGETS !== 'undefined') { 42 | window.DISQUSWIDGETS = undefined 43 | } 44 | if (Object.values(categories).length) { 45 | await loadDisqusCountScript() 46 | } 47 | 48 | return removeDisqusCountScript 49 | }, [categories]) 50 | 51 | return html`` 52 | } 53 | -------------------------------------------------------------------------------- /app/components/disqus/disqusThread.js: -------------------------------------------------------------------------------- 1 | import { html, useEffect, useState } from '../../lib/htm-preact.js' 2 | import { to } from '../../utils/to.js' 3 | 4 | const conf = window.appConf 5 | const DISQUS_THREAD_URL = `//${conf.shortname}.disqus.com/embed.js` 6 | 7 | export const DisqusThread = ({ articleId, articleTitle }) => { 8 | const [disqusThreadScript, setDisqusThreadScript] = useState(null) 9 | 10 | const loadDisqusThreadScript = async () => { 11 | const [loadError, loadedScript] = await to( 12 | new Promise(function (resolve, reject) { 13 | const head = document.getElementsByTagName('head')[0] 14 | const script = document.createElement('script') 15 | 16 | script.type = 'text/javascript' 17 | script.addEventListener('load', function (scriptData) { 18 | resolve(scriptData) 19 | }) 20 | script.defer = true 21 | script.className = 'disqus-thread-script' 22 | script.src = DISQUS_THREAD_URL 23 | head.appendChild(script) 24 | }) 25 | ) 26 | if (loadError) { 27 | console.log('failed loading disqus thread script', loadError) 28 | } else { 29 | setDisqusThreadScript(loadedScript) 30 | } 31 | } 32 | const removeDisqusThreadScript = () => { 33 | if (disqusThreadScript && disqusThreadScript.parentNode) { 34 | disqusThreadScript.parentNode.removeChild(disqusThreadScript) 35 | setDisqusThreadScript(null) 36 | } 37 | } 38 | 39 | useEffect(async () => { 40 | window.disqus_shortname = conf.shortname 41 | window.disqus_identifier = articleId 42 | window.disqus_title = articleTitle 43 | window.disqus_url = window.location.href 44 | 45 | if (typeof window.DISQUS !== 'undefined') { 46 | window.DISQUS.reset({ reload: true }) 47 | } else { 48 | await loadDisqusThreadScript() 49 | } 50 | return removeDisqusThreadScript 51 | }, [articleId, articleTitle]) 52 | 53 | return html`
` 54 | } 55 | -------------------------------------------------------------------------------- /app/components/form/baseInput.js: -------------------------------------------------------------------------------- 1 | import { html } from '../../lib/htm-preact.js' 2 | import capitalize from '../../utils/capitalize.js' 3 | 4 | export const BaseInput = ({ 5 | className = '', 6 | style = {}, 7 | name = '', 8 | type = 'text', 9 | placeholder = '', 10 | Input = 'input', 11 | property = '', 12 | min = 0, 13 | step = 1, 14 | value, 15 | required = false, 16 | onInput, 17 | }) => { 18 | return html`<${Input} 19 | value=${value} 20 | onInput=${onInput} 21 | style=${style} 22 | class=${className} 23 | name=${name} 24 | type=${type} 25 | placeholder=${placeholder ? placeholder : capitalize(name)} 26 | data-property=${property ? property : name} 27 | min=${min} 28 | step=${step} 29 | required=${Boolean(required)} 30 | />` 31 | } 32 | -------------------------------------------------------------------------------- /app/components/layout/footer.js: -------------------------------------------------------------------------------- 1 | import { html } from '../../lib/htm-preact.js' 2 | import { avoidReload } from '../../utils/avoidReload.js' 3 | import prefixUriIfNeeded from '../../utils/prefixUriIfNeeded.js' 4 | const conf = window.appConf 5 | 6 | export const Footer = ({ 7 | article = {}, 8 | category = {}, 9 | articles, 10 | menuVisible, 11 | }) => { 12 | return html` 13 | 213 | 310 | ` 311 | } 312 | -------------------------------------------------------------------------------- /app/components/layout/menu.js: -------------------------------------------------------------------------------- 1 | import { html, useEffect, useState } from '../../lib/htm-preact.js' 2 | import { avoidReload } from '../../utils/avoidReload.js' 3 | import prefixUriIfNeeded from '../../utils/prefixUriIfNeeded.js' 4 | 5 | export const Menu = ({ categories, articles, menuVisible }) => { 6 | const [activeCategory, setActiveCategory] = useState(false) 7 | const toggleCategory = (event) => { 8 | const category = event.target.dataset.category 9 | setActiveCategory(category !== activeCategory ? category : false) 10 | } 11 | useEffect(() => { 12 | setActiveCategory(Object.values(categories)?.[0]?.id) 13 | }, [categories]) 14 | return html` 135 | ` 221 | } 222 | -------------------------------------------------------------------------------- /app/components/layout/menuBurger.js: -------------------------------------------------------------------------------- 1 | import { html } from '../../lib/htm-preact.js' 2 | 3 | export const MenuBurger = ({ toggleMenuVisible }) => { 4 | return html` 42 | ` 47 | } 48 | -------------------------------------------------------------------------------- /app/components/layout/page.js: -------------------------------------------------------------------------------- 1 | import { html } from '../../lib/htm-preact.js' 2 | import { Menu } from './menu.js' 3 | import { Sidebar } from './sidebar.js' 4 | import { buttonsStyles } from '../../styles/buttons.js' 5 | import { blocksStyles } from '../../styles/blocks.js' 6 | import { MenuBurger } from './menuBurger.js' 7 | import { useCategoriesAndArticles } from '../../hooks/useCategoriesAndArticles.js' 8 | import { usePageMeta } from '../../hooks/usePageMeta.js' 9 | import { useMenuVisible } from '../../hooks/useMenuVisible.js' 10 | 11 | export const Page = ({ 12 | title, 13 | subtitle, 14 | description, 15 | sidebarImage, 16 | showLinks, 17 | children, 18 | }) => { 19 | const { categories, articles } = useCategoriesAndArticles() 20 | const { menuVisible, toggleMenuVisible } = useMenuVisible() 21 | 22 | usePageMeta(title, subtitle) 23 | 24 | return html` 105 |
106 | <${MenuBurger} toggleMenuVisible=${toggleMenuVisible} /> 107 | <${Menu} 108 | articles=${articles} 109 | categories=${categories} 110 | menuVisible=${menuVisible} 111 | /> 112 |
117 | <${Sidebar} 118 | title=${title} 119 | subtitle=${subtitle} 120 | description=${description} 121 | sidebarImage=${sidebarImage} 122 | menuVisible=${menuVisible} 123 | showLinks=${showLinks} 124 | /> 125 |
130 | ${children} 131 |
132 |
133 |
` 134 | } 135 | -------------------------------------------------------------------------------- /app/components/layout/postsAndCategories.js: -------------------------------------------------------------------------------- 1 | import { html } from '../../lib/htm-preact.js' 2 | import { DisqusCount } from '../disqus/disqusCount.js' 3 | import { Article as ArticleBlock } from '../blocks/article.js' 4 | import { Category as CategoryBlock } from '../blocks/category.js' 5 | import { getActiveItemId } from '../../utils/path.js' 6 | import { useCategoriesAndArticles } from '../../hooks/useCategoriesAndArticles.js' 7 | import { useActivePanel } from '../../hooks/useActivePanel.js' 8 | 9 | export const PostsAndCategories = ({ state, dispatch }) => { 10 | const { categories, articles } = useCategoriesAndArticles() 11 | const activeCategoryId = state?.activeItemId ?? getActiveItemId() 12 | 13 | const { activePanel, setActivePanel } = useActivePanel() 14 | const handleSelectPanel = (event) => { 15 | const selectedPanel = event.target.dataset.panel 16 | setActivePanel(selectedPanel) 17 | } 18 | 19 | return html` 70 |
71 | 80 | 89 |
90 |
95 | ${Object.values(articles) 96 | .filter( 97 | (article) => 98 | !activeCategoryId || 99 | article.categoryId === activeCategoryId 100 | ) 101 | .map( 102 | (article) => html` 103 | <${ArticleBlock} 104 | key=${article.id} 105 | article=${article} 106 | category=${categories[article.categoryId]} 107 | /> 108 | ` 109 | )} 110 |
111 |
116 | ${Object.values(categories).map( 117 | (category) => html` 118 | <${CategoryBlock} 119 | key=${category.id} 120 | category=${category} 121 | setActivePanel=${setActivePanel} 122 | /> 123 | ` 124 | )} 125 |
126 | <${DisqusCount} categories=${categories} /> ` 127 | } 128 | -------------------------------------------------------------------------------- /app/components/layout/sidebar.js: -------------------------------------------------------------------------------- 1 | import { html } from '../../lib/htm-preact.js' 2 | 3 | export const Sidebar = ({ 4 | title, 5 | subtitle, 6 | description, 7 | sidebarImage, 8 | menuVisible, 9 | showLinks, 10 | }) => { 11 | return html` 160 | ` 188 | } 189 | -------------------------------------------------------------------------------- /app/hooks/useActivePanel.js: -------------------------------------------------------------------------------- 1 | import { useState } from '../lib/htm-preact.js' 2 | 3 | export const useActivePanel = () => { 4 | const [activePanel, setActivePanel] = useState('posts') 5 | return { activePanel, setActivePanel } 6 | } 7 | -------------------------------------------------------------------------------- /app/hooks/useArticleText.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from '../lib/htm-preact.js' 2 | import { Drive } from '../lib/drive.js' 3 | 4 | export const useArticleText = (articleId) => { 5 | const [isFetching, setIsFetching] = useState({}) 6 | const [texts, setTexts] = useState({}) 7 | 8 | const article = texts?.[articleId] ?? '' 9 | const isFetchingArticle = isFetching?.[articleId] 10 | 11 | useEffect(async () => { 12 | if (!texts?.[articleId] && !isFetchingArticle) { 13 | setIsFetching({ 14 | ...isFetching, 15 | [articleId]: true, 16 | }) 17 | const [fetchArticleError, articleHtml] = await Drive.fetchArticle( 18 | articleId 19 | ) 20 | if (fetchArticleError) { 21 | throw new Error(fetchArticleError) 22 | } 23 | setIsFetching({ 24 | ...isFetching, 25 | [articleId]: false, 26 | }) 27 | setTexts({ 28 | ...texts, 29 | [articleId]: articleHtml, 30 | }) 31 | 32 | document.getElementById('article-header')?.scrollIntoView() 33 | } 34 | }, [articleId, texts, isFetchingArticle]) 35 | return article 36 | } 37 | -------------------------------------------------------------------------------- /app/hooks/useCategoriesAndArticles.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from '../lib/htm-preact.js' 2 | import { Drive } from '../lib/drive.js' 3 | const conf = window.appConf 4 | const dashboardId = conf.dashboardId 5 | 6 | export const useCategoriesAndArticles = () => { 7 | const [categoriesAndArticles, setCategoriesAndArticles] = useState({ 8 | categories: {}, 9 | articles: {}, 10 | }) 11 | 12 | useEffect(async () => { 13 | const [getCategoriesAndArticlesError, response] = 14 | await Drive.fetchCategories(dashboardId) 15 | if (getCategoriesAndArticlesError) { 16 | throw new Error(getCategoriesAndArticlesError) 17 | } 18 | const { categories, articles } = response 19 | setCategoriesAndArticles({ categories, articles }) 20 | }, []) 21 | 22 | return categoriesAndArticles 23 | } 24 | -------------------------------------------------------------------------------- /app/hooks/useMenuVisible.js: -------------------------------------------------------------------------------- 1 | import { useState } from '../lib/htm-preact.js' 2 | 3 | export const useMenuVisible = () => { 4 | const [menuVisible, setMenuVisible] = useState( 5 | !(typeof window !== 'undefined' && window.innerWidth < 769) 6 | ) 7 | const toggleMenuVisible = () => { 8 | setMenuVisible(!menuVisible) 9 | } 10 | return { menuVisible, toggleMenuVisible } 11 | } 12 | -------------------------------------------------------------------------------- /app/hooks/usePageMeta.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from '../lib/htm-preact.js' 2 | 3 | export const usePageMeta = (title, subtitle) => { 4 | useEffect(() => { 5 | document.title = title 6 | ? `${title} - React Drive CMS` 7 | : 'React Drive CMS' 8 | }, [title]) 9 | 10 | useEffect(() => { 11 | document 12 | ?.querySelector('meta[name="description"]') 13 | ?.setAttribute('content', subtitle) 14 | }, [subtitle]) 15 | } 16 | -------------------------------------------------------------------------------- /app/lib/api.js: -------------------------------------------------------------------------------- 1 | export const Api = { 2 | async call( 3 | url, 4 | options = { 5 | method: 'GET', 6 | credentials: 'include', 7 | headers: {}, 8 | } 9 | ) { 10 | const { method, credentials, headers } = options 11 | 12 | if (!credentials) { 13 | options = { ...options, credentials: 'include' } 14 | } 15 | 16 | options = Object.assign({}, options, { 17 | headers, 18 | }) 19 | 20 | return fetch(url, options) 21 | }, 22 | 23 | async get( 24 | url, 25 | options = { 26 | method: 'GET', 27 | credentials: 'include', 28 | } 29 | ) { 30 | return this.call(url, { ...options, method: 'GET' }) 31 | }, 32 | 33 | async post( 34 | url, 35 | options = { 36 | method: 'POST', 37 | credentials: 'include', 38 | } 39 | ) { 40 | return this.call(url, { ...options, method: 'POST' }) 41 | }, 42 | } 43 | -------------------------------------------------------------------------------- /app/lib/drive.js: -------------------------------------------------------------------------------- 1 | import { Api } from './api.js' 2 | import { to } from '../utils/to.js' 3 | const conf = window.appConf 4 | 5 | export const Drive = { 6 | ...Api, 7 | driveExportUrl: 'https://drive.google.com/uc?export=view&id=', 8 | isFetchingCategories: false, 9 | 10 | async getSpreadsheet(fileId) { 11 | return await to( 12 | this.get( 13 | `https://sheets.googleapis.com/v4/spreadsheets/${fileId}/values/Posts?alt=json&key=${conf.googleApiKey}`, 14 | { 15 | credentials: 'omit', 16 | } 17 | ).then((response) => response.json()) 18 | ) 19 | }, 20 | 21 | async getDocument(fileId) { 22 | return await to( 23 | this.get( 24 | `https://docs.google.com/feeds/download/documents/export/Export?id=${fileId}&exportFormat=html`, 25 | { 26 | credentials: 'omit', 27 | } 28 | ).then((response) => { 29 | return response.text() 30 | }) 31 | ) 32 | }, 33 | 34 | async fetchCategories(dashboardId) { 35 | const [getSpreadsheetError, spreadsheet] = await this.getSpreadsheet( 36 | dashboardId 37 | ) 38 | if (getSpreadsheetError) { 39 | console.log('getSpreadsheetError', getSpreadsheetError) 40 | return [getSpreadsheetError] 41 | } 42 | const rows = spreadsheet.values 43 | rows.shift() 44 | 45 | const categories = {} 46 | const articles = {} 47 | rows.forEach((row) => { 48 | articles[row?.[4]] = { 49 | id: row?.[4], 50 | title: row?.[0], 51 | subtitle: row?.[1], 52 | imageName: row?.[3], 53 | image: this.driveExportUrl + row?.[5], 54 | category: row?.[2], 55 | categoryId: this.slug(row?.[2], 'category'), 56 | lastUpdated: row?.[6], 57 | date: this.formatDate(row?.[6]), 58 | uri: `/articles/${row?.[4]}/${this.slug(row?.[0], 'article')}`, 59 | } 60 | const categoryId = articles[row?.[4]]?.categoryId 61 | const isExistingCategory = Object.values(categories).some( 62 | (category) => category.id === categoryId 63 | ) 64 | if (isExistingCategory) { 65 | categories[categoryId].articles.push(row?.[4]) 66 | } else { 67 | categories[categoryId] = { 68 | id: categoryId, 69 | title: row?.[2], 70 | imageName: row?.[3], 71 | image: articles[row?.[4]]?.image, 72 | articles: [row?.[4]], 73 | uri: `/categories/${categoryId}`, 74 | } 75 | } 76 | }) 77 | return [ 78 | null, 79 | { 80 | articles, 81 | categories, 82 | }, 83 | ] 84 | }, 85 | 86 | async fetchArticle(articleId) { 87 | const [getDocumentError, doc] = await this.getDocument(articleId) 88 | if (getDocumentError) { 89 | console.log('getDocumentError', getDocumentError) 90 | return [getDocumentError] 91 | } 92 | let styleStart = '' 94 | let splitStyleStart = doc.split(styleStart) 95 | let splitStyleEnd = splitStyleStart[1].split(styleEnd) 96 | 97 | let htmlStart = '${splitHtmlEnd[0]}
113 | `, 114 | ] 115 | }, 116 | 117 | slug(str, type = 'type') { 118 | str = str.replace(/^\s+|\s+$/g, '') 119 | str = str.toLowerCase() 120 | 121 | let from = 'ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;' 122 | let to = 'aaaaaeeeeeiiiiooooouuuunc------' 123 | for (let i = 0, l = from.length; i < l; i++) { 124 | str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) 125 | } 126 | 127 | str = str 128 | .replace(/[^a-z0-9 -]/g, '') 129 | .replace(/\s+/g, '-') 130 | .replace(/-+/g, '-') 131 | 132 | if (str.length < 4) { 133 | str = type + '_' + str 134 | } 135 | return str 136 | }, 137 | 138 | formatDate(lastUpdated) { 139 | const fullDateSplit = lastUpdated.split(' ') 140 | const dateSplit = fullDateSplit[0].split('/') 141 | const day = parseInt(dateSplit[0]) 142 | const month = dateSplit[1] 143 | const year = dateSplit[2] 144 | const monthNames = [ 145 | 'January', 146 | 'February', 147 | 'March', 148 | 'April', 149 | 'May', 150 | 'June', 151 | 'July', 152 | 'August', 153 | 'September', 154 | 'October', 155 | 'November', 156 | 'December', 157 | ] 158 | let daySuffix = 'th' 159 | switch (day) { 160 | case 1: 161 | daySuffix = 'st' 162 | break 163 | case 2: 164 | daySuffix = 'nd' 165 | break 166 | case 3: 167 | daySuffix = 'rd' 168 | break 169 | } 170 | return `${day}${daySuffix} of ${monthNames[month - 1]}` 171 | }, 172 | } 173 | -------------------------------------------------------------------------------- /app/lib/htm-preact.js: -------------------------------------------------------------------------------- 1 | // this is the file at https://unpkg.com/htm@3.1.0/preact/standalone.module.js saved here as backup 2 | var e, 3 | n, 4 | _, 5 | t, 6 | o, 7 | r, 8 | u, 9 | l = {}, 10 | i = [], 11 | c = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i 12 | 13 | function s(e, n) { 14 | for (var _ in n) e[_] = n[_] 15 | return e 16 | } 17 | 18 | function f(e) { 19 | var n = e.parentNode 20 | n && n.removeChild(e) 21 | } 22 | 23 | function a(n, _, t) { 24 | var o, 25 | r, 26 | u, 27 | l = {} 28 | for (u in _) 29 | 'key' == u ? (o = _[u]) : 'ref' == u ? (r = _[u]) : (l[u] = _[u]) 30 | if ( 31 | (arguments.length > 2 && 32 | (l.children = arguments.length > 3 ? e.call(arguments, 2) : t), 33 | 'function' == typeof n && null != n.defaultProps) 34 | ) 35 | for (u in n.defaultProps) void 0 === l[u] && (l[u] = n.defaultProps[u]) 36 | return p(n, l, o, r, null) 37 | } 38 | 39 | function p(e, t, o, r, u) { 40 | var l = { 41 | type: e, 42 | props: t, 43 | key: o, 44 | ref: r, 45 | __k: null, 46 | __: null, 47 | __b: 0, 48 | __e: null, 49 | __d: void 0, 50 | __c: null, 51 | __h: null, 52 | constructor: void 0, 53 | __v: null == u ? ++_ : u, 54 | } 55 | return null != n.vnode && n.vnode(l), l 56 | } 57 | 58 | function h(e) { 59 | return e.children 60 | } 61 | 62 | function d(e, n) { 63 | ;(this.props = e), (this.context = n) 64 | } 65 | 66 | function v(e, n) { 67 | if (null == n) return e.__ ? v(e.__, e.__.__k.indexOf(e) + 1) : null 68 | for (var _; n < e.__k.length; n++) 69 | if (null != (_ = e.__k[n]) && null != _.__e) return _.__e 70 | return 'function' == typeof e.type ? v(e) : null 71 | } 72 | 73 | function y(e) { 74 | var n, _ 75 | if (null != (e = e.__) && null != e.__c) { 76 | for (e.__e = e.__c.base = null, n = 0; n < e.__k.length; n++) 77 | if (null != (_ = e.__k[n]) && null != _.__e) { 78 | e.__e = e.__c.base = _.__e 79 | break 80 | } 81 | return y(e) 82 | } 83 | } 84 | 85 | function m(e) { 86 | ;((!e.__d && (e.__d = !0) && t.push(e) && !g.__r++) || 87 | r !== n.debounceRendering) && 88 | ((r = n.debounceRendering) || o)(g) 89 | } 90 | 91 | function g() { 92 | for (var e; (g.__r = t.length); ) 93 | (e = t.sort(function (e, n) { 94 | return e.__v.__b - n.__v.__b 95 | })), 96 | (t = []), 97 | e.some(function (e) { 98 | var n, _, t, o, r, u 99 | e.__d && 100 | ((r = (o = (n = e).__v).__e), 101 | (u = n.__P) && 102 | ((_ = []), 103 | ((t = s({}, o)).__v = o.__v + 1), 104 | P( 105 | u, 106 | o, 107 | t, 108 | n.__n, 109 | void 0 !== u.ownerSVGElement, 110 | null != o.__h ? [r] : null, 111 | _, 112 | null == r ? v(o) : r, 113 | o.__h 114 | ), 115 | D(_, o), 116 | o.__e != r && y(o))) 117 | }) 118 | } 119 | 120 | function k(e, n, _, t, o, r, u, c, s, f) { 121 | var a, 122 | d, 123 | y, 124 | m, 125 | g, 126 | k, 127 | x, 128 | H = (t && t.__k) || i, 129 | E = H.length 130 | for (_.__k = [], a = 0; a < n.length; a++) 131 | if ( 132 | null != 133 | (m = _.__k[a] = 134 | null == (m = n[a]) || 'boolean' == typeof m 135 | ? null 136 | : 'string' == typeof m || 137 | 'number' == typeof m || 138 | 'bigint' == typeof m 139 | ? p(null, m, null, null, m) 140 | : Array.isArray(m) 141 | ? p(h, { children: m }, null, null, null) 142 | : m.__b > 0 143 | ? p(m.type, m.props, m.key, null, m.__v) 144 | : m) 145 | ) { 146 | if ( 147 | ((m.__ = _), 148 | (m.__b = _.__b + 1), 149 | null === (y = H[a]) || 150 | (y && m.key == y.key && m.type === y.type)) 151 | ) 152 | H[a] = void 0 153 | else 154 | for (d = 0; d < E; d++) { 155 | if ((y = H[d]) && m.key == y.key && m.type === y.type) { 156 | H[d] = void 0 157 | break 158 | } 159 | y = null 160 | } 161 | P(e, m, (y = y || l), o, r, u, c, s, f), 162 | (g = m.__e), 163 | (d = m.ref) && 164 | y.ref != d && 165 | (x || (x = []), 166 | y.ref && x.push(y.ref, null, m), 167 | x.push(d, m.__c || g, m)), 168 | null != g 169 | ? (null == k && (k = g), 170 | 'function' == typeof m.type && 171 | null != m.__k && 172 | m.__k === y.__k 173 | ? (m.__d = s = b(m, s, e)) 174 | : (s = C(e, m, y, H, g, s)), 175 | f || 'option' !== _.type 176 | ? 'function' == typeof _.type && (_.__d = s) 177 | : (e.value = '')) 178 | : s && y.__e == s && s.parentNode != e && (s = v(y)) 179 | } 180 | for (_.__e = k, a = E; a--; ) 181 | null != H[a] && 182 | ('function' == typeof _.type && 183 | null != H[a].__e && 184 | H[a].__e == _.__d && 185 | (_.__d = v(t, a + 1)), 186 | U(H[a], H[a])) 187 | if (x) for (a = 0; a < x.length; a++) T(x[a], x[++a], x[++a]) 188 | } 189 | 190 | function b(e, n, _) { 191 | var t, o 192 | for (t = 0; t < e.__k.length; t++) 193 | (o = e.__k[t]) && 194 | ((o.__ = e), 195 | (n = 196 | 'function' == typeof o.type 197 | ? b(o, n, _) 198 | : C(_, o, o, e.__k, o.__e, n))) 199 | return n 200 | } 201 | 202 | function C(e, n, _, t, o, r) { 203 | var u, l, i 204 | if (void 0 !== n.__d) (u = n.__d), (n.__d = void 0) 205 | else if (null == _ || o != r || null == o.parentNode) 206 | e: if (null == r || r.parentNode !== e) e.appendChild(o), (u = null) 207 | else { 208 | for (l = r, i = 0; (l = l.nextSibling) && i < t.length; i += 2) 209 | if (l == o) break e 210 | e.insertBefore(o, r), (u = r) 211 | } 212 | return void 0 !== u ? u : o.nextSibling 213 | } 214 | 215 | function x(e, n, _) { 216 | '-' === n[0] 217 | ? e.setProperty(n, _) 218 | : (e[n] = 219 | null == _ ? '' : 'number' != typeof _ || c.test(n) ? _ : _ + 'px') 220 | } 221 | 222 | function H(e, n, _, t, o) { 223 | var r 224 | e: if ('style' === n) 225 | if ('string' == typeof _) e.style.cssText = _ 226 | else { 227 | if (('string' == typeof t && (e.style.cssText = t = ''), t)) 228 | for (n in t) (_ && n in _) || x(e.style, n, '') 229 | if (_) for (n in _) (t && _[n] === t[n]) || x(e.style, n, _[n]) 230 | } 231 | else if ('o' === n[0] && 'n' === n[1]) 232 | (r = n !== (n = n.replace(/Capture$/, ''))), 233 | (n = n.toLowerCase() in e ? n.toLowerCase().slice(2) : n.slice(2)), 234 | e.l || (e.l = {}), 235 | (e.l[n + r] = _), 236 | _ 237 | ? t || e.addEventListener(n, r ? S : E, r) 238 | : e.removeEventListener(n, r ? S : E, r) 239 | else if ('dangerouslySetInnerHTML' !== n) { 240 | if (o) n = n.replace(/xlink[H:h]/, 'h').replace(/sName$/, 's') 241 | else if ( 242 | 'href' !== n && 243 | 'list' !== n && 244 | 'form' !== n && 245 | 'tabIndex' !== n && 246 | 'download' !== n && 247 | n in e 248 | ) 249 | try { 250 | e[n] = null == _ ? '' : _ 251 | break e 252 | } catch (e) {} 253 | 'function' == typeof _ || 254 | (null != _ && (!1 !== _ || ('a' === n[0] && 'r' === n[1])) 255 | ? e.setAttribute(n, _) 256 | : e.removeAttribute(n)) 257 | } 258 | } 259 | 260 | function E(e) { 261 | this.l[e.type + !1](n.event ? n.event(e) : e) 262 | } 263 | 264 | function S(e) { 265 | this.l[e.type + !0](n.event ? n.event(e) : e) 266 | } 267 | 268 | function P(e, _, t, o, r, u, l, i, c) { 269 | var f, 270 | a, 271 | p, 272 | v, 273 | y, 274 | m, 275 | g, 276 | b, 277 | C, 278 | x, 279 | H, 280 | E = _.type 281 | if (void 0 !== _.constructor) return null 282 | null != t.__h && 283 | ((c = t.__h), (i = _.__e = t.__e), (_.__h = null), (u = [i])), 284 | (f = n.__b) && f(_) 285 | try { 286 | e: if ('function' == typeof E) { 287 | if ( 288 | ((b = _.props), 289 | (C = (f = E.contextType) && o[f.__c]), 290 | (x = f ? (C ? C.props.value : f.__) : o), 291 | t.__c 292 | ? (g = (a = _.__c = t.__c).__ = a.__E) 293 | : ('prototype' in E && E.prototype.render 294 | ? (_.__c = a = new E(b, x)) 295 | : ((_.__c = a = new d(b, x)), 296 | (a.constructor = E), 297 | (a.render = A)), 298 | C && C.sub(a), 299 | (a.props = b), 300 | a.state || (a.state = {}), 301 | (a.context = x), 302 | (a.__n = o), 303 | (p = a.__d = !0), 304 | (a.__h = [])), 305 | null == a.__s && (a.__s = a.state), 306 | null != E.getDerivedStateFromProps && 307 | (a.__s == a.state && (a.__s = s({}, a.__s)), 308 | s(a.__s, E.getDerivedStateFromProps(b, a.__s))), 309 | (v = a.props), 310 | (y = a.state), 311 | p) 312 | ) 313 | null == E.getDerivedStateFromProps && 314 | null != a.componentWillMount && 315 | a.componentWillMount(), 316 | null != a.componentDidMount && 317 | a.__h.push(a.componentDidMount) 318 | else { 319 | if ( 320 | (null == E.getDerivedStateFromProps && 321 | b !== v && 322 | null != a.componentWillReceiveProps && 323 | a.componentWillReceiveProps(b, x), 324 | (!a.__e && 325 | null != a.shouldComponentUpdate && 326 | !1 === a.shouldComponentUpdate(b, a.__s, x)) || 327 | _.__v === t.__v) 328 | ) { 329 | ;(a.props = b), 330 | (a.state = a.__s), 331 | _.__v !== t.__v && (a.__d = !1), 332 | (a.__v = _), 333 | (_.__e = t.__e), 334 | (_.__k = t.__k), 335 | _.__k.forEach(function (e) { 336 | e && (e.__ = _) 337 | }), 338 | a.__h.length && l.push(a) 339 | break e 340 | } 341 | null != a.componentWillUpdate && 342 | a.componentWillUpdate(b, a.__s, x), 343 | null != a.componentDidUpdate && 344 | a.__h.push(function () { 345 | a.componentDidUpdate(v, y, m) 346 | }) 347 | } 348 | ;(a.context = x), 349 | (a.props = b), 350 | (a.state = a.__s), 351 | (f = n.__r) && f(_), 352 | (a.__d = !1), 353 | (a.__v = _), 354 | (a.__P = e), 355 | (f = a.render(a.props, a.state, a.context)), 356 | (a.state = a.__s), 357 | null != a.getChildContext && 358 | (o = s(s({}, o), a.getChildContext())), 359 | p || 360 | null == a.getSnapshotBeforeUpdate || 361 | (m = a.getSnapshotBeforeUpdate(v, y)), 362 | (H = 363 | null != f && f.type === h && null == f.key 364 | ? f.props.children 365 | : f), 366 | k(e, Array.isArray(H) ? H : [H], _, t, o, r, u, l, i, c), 367 | (a.base = _.__e), 368 | (_.__h = null), 369 | a.__h.length && l.push(a), 370 | g && (a.__E = a.__ = null), 371 | (a.__e = !1) 372 | } else 373 | null == u && _.__v === t.__v 374 | ? ((_.__k = t.__k), (_.__e = t.__e)) 375 | : (_.__e = w(t.__e, _, t, o, r, u, l, c)) 376 | ;(f = n.diffed) && f(_) 377 | } catch (e) { 378 | ;(_.__v = null), 379 | (c || null != u) && 380 | ((_.__e = i), (_.__h = !!c), (u[u.indexOf(i)] = null)), 381 | n.__e(e, _, t) 382 | } 383 | } 384 | 385 | function D(e, _) { 386 | n.__c && n.__c(_, e), 387 | e.some(function (_) { 388 | try { 389 | ;(e = _.__h), 390 | (_.__h = []), 391 | e.some(function (e) { 392 | e.call(_) 393 | }) 394 | } catch (e) { 395 | n.__e(e, _.__v) 396 | } 397 | }) 398 | } 399 | 400 | function w(n, _, t, o, r, u, i, c) { 401 | var s, 402 | a, 403 | p, 404 | h = t.props, 405 | d = _.props, 406 | y = _.type, 407 | m = 0 408 | if (('svg' === y && (r = !0), null != u)) 409 | for (; m < u.length; m++) 410 | if ( 411 | (s = u[m]) && 412 | (s === n || (y ? s.localName == y : 3 == s.nodeType)) 413 | ) { 414 | ;(n = s), (u[m] = null) 415 | break 416 | } 417 | if (null == n) { 418 | if (null === y) return document.createTextNode(d) 419 | ;(n = r 420 | ? document.createElementNS('http://www.w3.org/2000/svg', y) 421 | : document.createElement(y, d.is && d)), 422 | (u = null), 423 | (c = !1) 424 | } 425 | if (null === y) h === d || (c && n.data === d) || (n.data = d) 426 | else { 427 | if ( 428 | ((u = u && e.call(n.childNodes)), 429 | (a = (h = t.props || l).dangerouslySetInnerHTML), 430 | (p = d.dangerouslySetInnerHTML), 431 | !c) 432 | ) { 433 | if (null != u) 434 | for (h = {}, m = 0; m < n.attributes.length; m++) 435 | h[n.attributes[m].name] = n.attributes[m].value 436 | ;(p || a) && 437 | ((p && 438 | ((a && p.__html == a.__html) || 439 | p.__html === n.innerHTML)) || 440 | (n.innerHTML = (p && p.__html) || '')) 441 | } 442 | if ( 443 | ((function (e, n, _, t, o) { 444 | var r 445 | for (r in _) 446 | 'children' === r || 447 | 'key' === r || 448 | r in n || 449 | H(e, r, null, _[r], t) 450 | for (r in n) 451 | (o && 'function' != typeof n[r]) || 452 | 'children' === r || 453 | 'key' === r || 454 | 'value' === r || 455 | 'checked' === r || 456 | _[r] === n[r] || 457 | H(e, r, n[r], _[r], t) 458 | })(n, d, h, r, c), 459 | p) 460 | ) 461 | _.__k = [] 462 | else if ( 463 | ((m = _.props.children), 464 | k( 465 | n, 466 | Array.isArray(m) ? m : [m], 467 | _, 468 | t, 469 | o, 470 | r && 'foreignObject' !== y, 471 | u, 472 | i, 473 | u ? u[0] : t.__k && v(t, 0), 474 | c 475 | ), 476 | null != u) 477 | ) 478 | for (m = u.length; m--; ) null != u[m] && f(u[m]) 479 | c || 480 | ('value' in d && 481 | void 0 !== (m = d.value) && 482 | (m !== n.value || ('progress' === y && !m)) && 483 | H(n, 'value', m, h.value, !1), 484 | 'checked' in d && 485 | void 0 !== (m = d.checked) && 486 | m !== n.checked && 487 | H(n, 'checked', m, h.checked, !1)) 488 | } 489 | return n 490 | } 491 | 492 | function T(e, _, t) { 493 | try { 494 | 'function' == typeof e ? e(_) : (e.current = _) 495 | } catch (e) { 496 | n.__e(e, t) 497 | } 498 | } 499 | 500 | function U(e, _, t) { 501 | var o, r 502 | if ( 503 | (n.unmount && n.unmount(e), 504 | (o = e.ref) && ((o.current && o.current !== e.__e) || T(o, null, _)), 505 | null != (o = e.__c)) 506 | ) { 507 | if (o.componentWillUnmount) 508 | try { 509 | o.componentWillUnmount() 510 | } catch (e) { 511 | n.__e(e, _) 512 | } 513 | o.base = o.__P = null 514 | } 515 | if ((o = e.__k)) 516 | for (r = 0; r < o.length; r++) 517 | o[r] && U(o[r], _, 'function' != typeof e.type) 518 | t || null == e.__e || f(e.__e), (e.__e = e.__d = void 0) 519 | } 520 | 521 | function A(e, n, _) { 522 | return this.constructor(e, _) 523 | } 524 | 525 | function M(_, t, o) { 526 | var r, u, i 527 | n.__ && n.__(_, t), 528 | (u = (r = 'function' == typeof o) ? null : (o && o.__k) || t.__k), 529 | (i = []), 530 | P( 531 | t, 532 | (_ = ((!r && o) || t).__k = a(h, null, [_])), 533 | u || l, 534 | l, 535 | void 0 !== t.ownerSVGElement, 536 | !r && o 537 | ? [o] 538 | : u 539 | ? null 540 | : t.firstChild 541 | ? e.call(t.childNodes) 542 | : null, 543 | i, 544 | !r && o ? o : u ? u.__e : t.firstChild, 545 | r 546 | ), 547 | D(i, _) 548 | } 549 | 550 | function F(e, n) { 551 | var _ = { 552 | __c: (n = '__cC' + u++), 553 | __: e, 554 | Consumer: function (e, n) { 555 | return e.children(n) 556 | }, 557 | Provider: function (e) { 558 | var _, t 559 | return ( 560 | this.getChildContext || 561 | ((_ = []), 562 | ((t = {})[n] = this), 563 | (this.getChildContext = function () { 564 | return t 565 | }), 566 | (this.shouldComponentUpdate = function (e) { 567 | this.props.value !== e.value && _.some(m) 568 | }), 569 | (this.sub = function (e) { 570 | _.push(e) 571 | var n = e.componentWillUnmount 572 | e.componentWillUnmount = function () { 573 | _.splice(_.indexOf(e), 1), n && n.call(e) 574 | } 575 | })), 576 | e.children 577 | ) 578 | }, 579 | } 580 | return (_.Provider.__ = _.Consumer.contextType = _) 581 | } 582 | 583 | ;(e = i.slice), 584 | (n = { 585 | __e: function (e, n) { 586 | for (var _, t, o; (n = n.__); ) 587 | if ((_ = n.__c) && !_.__) 588 | try { 589 | if ( 590 | ((t = _.constructor) && 591 | null != t.getDerivedStateFromError && 592 | (_.setState(t.getDerivedStateFromError(e)), 593 | (o = _.__d)), 594 | null != _.componentDidCatch && 595 | (_.componentDidCatch(e), (o = _.__d)), 596 | o) 597 | ) 598 | return (_.__E = _) 599 | } catch (n) { 600 | e = n 601 | } 602 | throw e 603 | }, 604 | }), 605 | (_ = 0), 606 | (d.prototype.setState = function (e, n) { 607 | var _ 608 | ;(_ = 609 | null != this.__s && this.__s !== this.state 610 | ? this.__s 611 | : (this.__s = s({}, this.state))), 612 | 'function' == typeof e && (e = e(s({}, _), this.props)), 613 | e && s(_, e), 614 | null != e && this.__v && (n && this.__h.push(n), m(this)) 615 | }), 616 | (d.prototype.forceUpdate = function (e) { 617 | this.__v && ((this.__e = !0), e && this.__h.push(e), m(this)) 618 | }), 619 | (d.prototype.render = h), 620 | (t = []), 621 | (o = 622 | 'function' == typeof Promise 623 | ? Promise.prototype.then.bind(Promise.resolve()) 624 | : setTimeout), 625 | (g.__r = 0), 626 | (u = 0) 627 | var L, 628 | N, 629 | W, 630 | R = 0, 631 | I = [], 632 | O = n.__b, 633 | V = n.__r, 634 | q = n.diffed, 635 | B = n.__c, 636 | $ = n.unmount 637 | 638 | function j(e, _) { 639 | n.__h && n.__h(N, e, R || _), (R = 0) 640 | var t = N.__H || (N.__H = { __: [], __h: [] }) 641 | return e >= t.__.length && t.__.push({}), t.__[e] 642 | } 643 | 644 | function G(e) { 645 | return (R = 1), z(ie, e) 646 | } 647 | 648 | function z(e, n, _) { 649 | var t = j(L++, 2) 650 | return ( 651 | (t.t = e), 652 | t.__c || 653 | ((t.__ = [ 654 | _ ? _(n) : ie(void 0, n), 655 | function (e) { 656 | var n = t.t(t.__[0], e) 657 | t.__[0] !== n && ((t.__ = [n, t.__[1]]), t.__c.setState({})) 658 | }, 659 | ]), 660 | (t.__c = N)), 661 | t.__ 662 | ) 663 | } 664 | 665 | function J(e, _) { 666 | var t = j(L++, 3) 667 | !n.__s && le(t.__H, _) && ((t.__ = e), (t.__H = _), N.__H.__h.push(t)) 668 | } 669 | 670 | function K(e, _) { 671 | var t = j(L++, 4) 672 | !n.__s && le(t.__H, _) && ((t.__ = e), (t.__H = _), N.__h.push(t)) 673 | } 674 | 675 | function Q(e) { 676 | return ( 677 | (R = 5), 678 | Y(function () { 679 | return { current: e } 680 | }, []) 681 | ) 682 | } 683 | 684 | function X(e, n, _) { 685 | ;(R = 6), 686 | K( 687 | function () { 688 | 'function' == typeof e ? e(n()) : e && (e.current = n()) 689 | }, 690 | null == _ ? _ : _.concat(e) 691 | ) 692 | } 693 | 694 | function Y(e, n) { 695 | var _ = j(L++, 7) 696 | return le(_.__H, n) && ((_.__ = e()), (_.__H = n), (_.__h = e)), _.__ 697 | } 698 | 699 | function Z(e, n) { 700 | return ( 701 | (R = 8), 702 | Y(function () { 703 | return e 704 | }, n) 705 | ) 706 | } 707 | 708 | function ee(e) { 709 | var n = N.context[e.__c], 710 | _ = j(L++, 9) 711 | return ( 712 | (_.c = e), 713 | n ? (null == _.__ && ((_.__ = !0), n.sub(N)), n.props.value) : e.__ 714 | ) 715 | } 716 | 717 | function ne(e, _) { 718 | n.useDebugValue && n.useDebugValue(_ ? _(e) : e) 719 | } 720 | 721 | function _e(e) { 722 | var n = j(L++, 10), 723 | _ = G() 724 | return ( 725 | (n.__ = e), 726 | N.componentDidCatch || 727 | (N.componentDidCatch = function (e) { 728 | n.__ && n.__(e), _[1](e) 729 | }), 730 | [ 731 | _[0], 732 | function () { 733 | _[1](void 0) 734 | }, 735 | ] 736 | ) 737 | } 738 | 739 | function te() { 740 | I.forEach(function (e) { 741 | if (e.__P) 742 | try { 743 | e.__H.__h.forEach(re), e.__H.__h.forEach(ue), (e.__H.__h = []) 744 | } catch (_) { 745 | ;(e.__H.__h = []), n.__e(_, e.__v) 746 | } 747 | }), 748 | (I = []) 749 | } 750 | 751 | ;(n.__b = function (e) { 752 | ;(N = null), O && O(e) 753 | }), 754 | (n.__r = function (e) { 755 | V && V(e), (L = 0) 756 | var n = (N = e.__c).__H 757 | n && (n.__h.forEach(re), n.__h.forEach(ue), (n.__h = [])) 758 | }), 759 | (n.diffed = function (e) { 760 | q && q(e) 761 | var _ = e.__c 762 | _ && 763 | _.__H && 764 | _.__H.__h.length && 765 | ((1 !== I.push(_) && W === n.requestAnimationFrame) || 766 | ( 767 | (W = n.requestAnimationFrame) || 768 | function (e) { 769 | var n, 770 | _ = function () { 771 | clearTimeout(t), 772 | oe && cancelAnimationFrame(n), 773 | setTimeout(e) 774 | }, 775 | t = setTimeout(_, 100) 776 | oe && (n = requestAnimationFrame(_)) 777 | } 778 | )(te)), 779 | (N = void 0) 780 | }), 781 | (n.__c = function (e, _) { 782 | _.some(function (e) { 783 | try { 784 | e.__h.forEach(re), 785 | (e.__h = e.__h.filter(function (e) { 786 | return !e.__ || ue(e) 787 | })) 788 | } catch (t) { 789 | _.some(function (e) { 790 | e.__h && (e.__h = []) 791 | }), 792 | (_ = []), 793 | n.__e(t, e.__v) 794 | } 795 | }), 796 | B && B(e, _) 797 | }), 798 | (n.unmount = function (e) { 799 | $ && $(e) 800 | var _ = e.__c 801 | if (_ && _.__H) 802 | try { 803 | _.__H.__.forEach(re) 804 | } catch (e) { 805 | n.__e(e, _.__v) 806 | } 807 | }) 808 | var oe = 'function' == typeof requestAnimationFrame 809 | 810 | function re(e) { 811 | var n = N 812 | 'function' == typeof e.__c && e.__c(), (N = n) 813 | } 814 | 815 | function ue(e) { 816 | var n = N 817 | ;(e.__c = e.__()), (N = n) 818 | } 819 | 820 | function le(e, n) { 821 | return ( 822 | !e || 823 | e.length !== n.length || 824 | n.some(function (n, _) { 825 | return n !== e[_] 826 | }) 827 | ) 828 | } 829 | 830 | function ie(e, n) { 831 | return 'function' == typeof n ? n(e) : n 832 | } 833 | 834 | var ce = function (e, n, _, t) { 835 | var o 836 | n[0] = 0 837 | for (var r = 1; r < n.length; r++) { 838 | var u = n[r++], 839 | l = n[r] ? ((n[0] |= u ? 1 : 2), _[n[r++]]) : n[++r] 840 | 3 === u 841 | ? (t[0] = l) 842 | : 4 === u 843 | ? (t[1] = Object.assign(t[1] || {}, l)) 844 | : 5 === u 845 | ? ((t[1] = t[1] || {})[n[++r]] = l) 846 | : 6 === u 847 | ? (t[1][n[++r]] += l + '') 848 | : u 849 | ? ((o = e.apply(l, ce(e, l, _, ['', null]))), 850 | t.push(o), 851 | l[0] ? (n[0] |= 2) : ((n[r - 2] = 0), (n[r] = o))) 852 | : t.push(l) 853 | } 854 | return t 855 | }, 856 | se = new Map(), 857 | fe = function (e) { 858 | var n = se.get(this) 859 | return ( 860 | n || ((n = new Map()), se.set(this, n)), 861 | (n = ce( 862 | this, 863 | n.get(e) || 864 | (n.set( 865 | e, 866 | (n = (function (e) { 867 | for ( 868 | var n, 869 | _, 870 | t = 1, 871 | o = '', 872 | r = '', 873 | u = [0], 874 | l = function (e) { 875 | 1 === t && 876 | (e || 877 | (o = o.replace( 878 | /^\s*\n\s*|\s*\n\s*$/g, 879 | '' 880 | ))) 881 | ? u.push(0, e, o) 882 | : 3 === t && (e || o) 883 | ? (u.push(3, e, o), (t = 2)) 884 | : 2 === t && '...' === o && e 885 | ? u.push(4, e, 0) 886 | : 2 === t && o && !e 887 | ? u.push(5, 0, !0, o) 888 | : t >= 5 && 889 | ((o || (!e && 5 === t)) && 890 | (u.push(t, 0, o, _), (t = 6)), 891 | e && 892 | (u.push(t, e, 0, _), 893 | (t = 6))), 894 | (o = '') 895 | }, 896 | i = 0; 897 | i < e.length; 898 | i++ 899 | ) { 900 | i && (1 === t && l(), l(i)) 901 | for (var c = 0; c < e[i].length; c++) 902 | (n = e[i][c]), 903 | 1 === t 904 | ? '<' === n 905 | ? (l(), (u = [u]), (t = 3)) 906 | : (o += n) 907 | : 4 === t 908 | ? '--' === o && '>' === n 909 | ? ((t = 1), (o = '')) 910 | : (o = n + o[0]) 911 | : r 912 | ? n === r 913 | ? (r = '') 914 | : (o += n) 915 | : '"' === n || "'" === n 916 | ? (r = n) 917 | : '>' === n 918 | ? (l(), (t = 1)) 919 | : t && 920 | ('=' === n 921 | ? ((t = 5), (_ = o), (o = '')) 922 | : '/' === n && 923 | (t < 5 || 924 | '>' === e[i][c + 1]) 925 | ? (l(), 926 | 3 === t && (u = u[0]), 927 | (t = u), 928 | (u = u[0]).push(2, 0, t), 929 | (t = 0)) 930 | : ' ' === n || 931 | '\t' === n || 932 | '\n' === n || 933 | '\r' === n 934 | ? (l(), (t = 2)) 935 | : (o += n)), 936 | 3 === t && 937 | '!--' === o && 938 | ((t = 4), (u = u[0])) 939 | } 940 | return l(), u 941 | })(e)) 942 | ), 943 | n), 944 | arguments, 945 | [] 946 | )).length > 1 947 | ? n 948 | : n[0] 949 | ) 950 | }.bind(a) 951 | export { 952 | a as h, 953 | fe as html, 954 | M as render, 955 | d as Component, 956 | F as createContext, 957 | G as useState, 958 | z as useReducer, 959 | J as useEffect, 960 | K as useLayoutEffect, 961 | Q as useRef, 962 | X as useImperativeHandle, 963 | Y as useMemo, 964 | Z as useCallback, 965 | ee as useContext, 966 | ne as useDebugValue, 967 | _e as useErrorBoundary, 968 | } 969 | -------------------------------------------------------------------------------- /app/lib/mail.js: -------------------------------------------------------------------------------- 1 | import { Api } from './api.js' 2 | const conf = window.appConf 3 | import jsonpCall from '../utils/jsonpCall.js' 4 | import { to } from '../utils/to.js' 5 | 6 | const APPS_SCRIPT_BASE_URL = `https://script.google.com/macros/s/${conf.sendContactMessageUrlId}/exec?` 7 | const IP_INFO_URL = 'https://ipinfo.io/json?token=' 8 | 9 | export const Mail = { 10 | ...Api, 11 | async getIpInfo() { 12 | return await to( 13 | new Promise((resolve, reject) => { 14 | try { 15 | jsonpCall(`${IP_INFO_URL}${conf.ipInfoToken}`, (ipInfo) => 16 | resolve(ipInfo) 17 | ) 18 | } catch (error) { 19 | reject(error) 20 | } 21 | }) 22 | ) 23 | }, 24 | async send(form) { 25 | const [ipError, ipInfo] = await this.getIpInfo() 26 | if (ipError) { 27 | return [ipError] 28 | } 29 | const message = { 30 | ...form, 31 | ip: ipInfo?.ip, 32 | location: ipInfo?.city + ' - ' + ipInfo?.country, 33 | } 34 | const queryParams = Object.keys(message) 35 | .map( 36 | (property) => 37 | `${property}=${encodeURIComponent(message[property])}` 38 | ) 39 | .join('&') 40 | 41 | return await to( 42 | new Promise((resolve, reject) => { 43 | try { 44 | jsonpCall(APPS_SCRIPT_BASE_URL + queryParams, (response) => 45 | resolve(response) 46 | ) 47 | } catch (error) { 48 | reject(error) 49 | } 50 | }) 51 | ) 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } -------------------------------------------------------------------------------- /app/routes/about.js: -------------------------------------------------------------------------------- 1 | import { html } from '../lib/htm-preact.js' 2 | import { Page } from '../components/layout/page.js' 3 | import prefixUriIfNeeded from '../utils/prefixUriIfNeeded.js' 4 | import { avoidReload } from '../utils/avoidReload.js' 5 | 6 | export const About = () => html` 7 | 61 | <${Page} 62 | title="About" 63 | subtitle="React Drive CMS Demo" 64 | description="An easy way to publish articles directly from Google Drive" 65 | sidebarImage=${prefixUriIfNeeded('/assets/default-about.jpg')} 66 | showLinks=${true} 67 | > 68 |
69 | 73 |
74 |

React Drive CMS Demo

75 |

76 | A demo site to showcase the use of Google Drive as a Content 77 | Management System. Write articles in Google Docs and publish 78 | them directly from there. 79 |

80 |

81 | Google Drive is the backend, only a few static files are 82 | hosted on GitHub Pages, and the content is displayed with 83 | React JS. 84 |

85 |
86 |
87 | 88 | 97 | 98 | ` 99 | -------------------------------------------------------------------------------- /app/routes/article.js: -------------------------------------------------------------------------------- 1 | import { html, useEffect } from '../lib/htm-preact.js' 2 | import { Menu } from '../components/layout/menu.js' 3 | import { blocksStyles } from '../styles/blocks.js' 4 | import { Footer } from '../components/layout/footer.js' 5 | import { DisqusThread } from '../components/disqus/disqusThread.js' 6 | import { getActiveItemId } from '../utils/path.js' 7 | import { MenuBurger } from '../components/layout/menuBurger.js' 8 | import { useCategoriesAndArticles } from '../hooks/useCategoriesAndArticles.js' 9 | import { usePageMeta } from '../hooks/usePageMeta.js' 10 | import { useArticleText } from '../hooks/useArticleText.js' 11 | import { useMenuVisible } from '../hooks/useMenuVisible.js' 12 | 13 | export const Article = ({ state, dispatch }) => { 14 | const { articles, categories } = useCategoriesAndArticles() 15 | const activeArticleId = state?.activeItemId ?? getActiveItemId() 16 | const activeArticle = articles?.[activeArticleId] ?? {} 17 | const activeText = useArticleText(activeArticleId) 18 | const category = categories?.[activeArticle?.categoryId] 19 | 20 | const title = activeArticle?.title 21 | const subtitle = activeArticle?.subtitle 22 | 23 | const { menuVisible, toggleMenuVisible } = useMenuVisible() 24 | usePageMeta(title, subtitle) 25 | 26 | return html` 144 |
145 | <${MenuBurger} toggleMenuVisible=${toggleMenuVisible} /> 146 | <${Menu} 147 | menuVisible=${menuVisible} 148 | articles=${articles} 149 | categories=${categories} 150 | /> 151 |
152 |
185 |
` 186 | } 187 | -------------------------------------------------------------------------------- /app/routes/category.js: -------------------------------------------------------------------------------- 1 | import { html } from '../lib/htm-preact.js' 2 | import { Page } from '../components/layout/page.js' 3 | import { PostsAndCategories } from '../components/layout/postsAndCategories.js' 4 | import { getActiveItemId } from '../utils/path.js' 5 | import { useCategoriesAndArticles } from '../hooks/useCategoriesAndArticles.js' 6 | 7 | export const Category = ({ state, dispatch }) => { 8 | const { categories } = useCategoriesAndArticles() 9 | const activeCategoryId = state?.activeItemId ?? getActiveItemId() 10 | const activeCategory = categories?.[activeCategoryId] ?? {} 11 | 12 | return html` <${Page} 13 | title=${activeCategory.title} 14 | sidebarImage=${activeCategory.image} 15 | > 16 | <${PostsAndCategories} state=${state} dispatch=${dispatch} /> 17 | ` 18 | } 19 | -------------------------------------------------------------------------------- /app/routes/contact.js: -------------------------------------------------------------------------------- 1 | import { html, useState, useMemo, useCallback } from '../lib/htm-preact.js' 2 | 3 | import { BaseInput } from '../components/form/baseInput.js' 4 | import { Page } from '../components/layout/page.js' 5 | import { inputStyles } from '../styles/input.js' 6 | import { Mail } from '../lib/mail.js' 7 | import prefixUriIfNeeded from '../utils/prefixUriIfNeeded.js' 8 | import { avoidReload } from '../utils/avoidReload.js' 9 | import debounce from '../utils/debounce.js' 10 | import { Spinner } from '../styles/Spinner.js' 11 | 12 | const validateEmailRegex = 13 | /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/ 14 | const requiredTexts = ['name', 'message'] 15 | 16 | export const Contact = ({ state, dispatch }) => { 17 | const [isSent, setIsSent] = useState(false) 18 | const [isSending, setIsSending] = useState(false) 19 | const [sentError, setSentError] = useState('') 20 | 21 | const [name, setName] = useState('') 22 | const [email, setEmail] = useState('') 23 | const [company, setCompany] = useState('') 24 | const [phone, setPhone] = useState('') 25 | const [message, setMessage] = useState('') 26 | 27 | const setText = { 28 | name: setName, 29 | company: setCompany, 30 | phone: setPhone, 31 | message: setMessage, 32 | } 33 | const texts = { name, company, phone, message } 34 | 35 | const [errorProperty, setErrorProperty] = useState('') 36 | const [formError, setFormError] = useState('') 37 | 38 | const validateText = useCallback( 39 | (property, value, isRequired) => { 40 | if (isRequired && value.length < 4) { 41 | setFormError('4 characters minimum.') 42 | setErrorProperty(property) 43 | return false 44 | } else if (errorProperty === property) { 45 | setFormError('') 46 | setErrorProperty('') 47 | } 48 | return true 49 | }, 50 | [errorProperty] 51 | ) 52 | 53 | const updateText = useCallback( 54 | (event) => { 55 | event.preventDefault() 56 | event.stopPropagation() 57 | const property = event.target.dataset.property 58 | const text = event.target.value 59 | setText[property](text) 60 | const required = Boolean(event.target.required) 61 | validateText(property, text, required) 62 | }, 63 | [validateText] 64 | ) 65 | 66 | const debouncedUpdateText = useMemo( 67 | () => debounce(updateText, 500), 68 | [updateText] 69 | ) 70 | 71 | const validateEmail = useCallback( 72 | (email) => { 73 | if (email.length < 4) { 74 | setFormError('4 characters minimum.') 75 | setErrorProperty('email') 76 | return false 77 | } else if (!validateEmailRegex.test(email)) { 78 | setFormError('Enter valid email') 79 | setErrorProperty('email') 80 | return false 81 | } else if (errorProperty === 'email') { 82 | setFormError('') 83 | setErrorProperty('') 84 | } 85 | return true 86 | }, 87 | [errorProperty] 88 | ) 89 | 90 | const updateEmail = useCallback( 91 | (event) => { 92 | event.preventDefault() 93 | event.stopPropagation() 94 | const email = event.target.value 95 | setEmail(email) 96 | validateEmail(email) 97 | }, 98 | [validateEmail] 99 | ) 100 | 101 | const debouncedUpdateEmail = useMemo( 102 | () => debounce(updateEmail, 500), 103 | [updateEmail] 104 | ) 105 | 106 | const sendMessage = async (event) => { 107 | event.preventDefault() 108 | event.stopPropagation() 109 | const emailIsValid = validateEmail(email) 110 | if (!emailIsValid) { 111 | return 112 | } 113 | const textsProperties = Object.keys(texts) 114 | 115 | for (let idx = 0; idx < textsProperties.length; idx++) { 116 | const property = textsProperties[idx] 117 | const isRequired = requiredTexts.includes(property) 118 | const textIsValid = validateText( 119 | property, 120 | texts[property], 121 | isRequired 122 | ) 123 | if (!textIsValid) { 124 | return 125 | } 126 | } 127 | if (isSending) { 128 | return false 129 | } 130 | setIsSending(true) 131 | setTimeout(() => { 132 | setIsSending(false) 133 | setSentError('timeout error, no response from server') 134 | }, 10000) 135 | const [sendError, sent] = await Mail.send({ 136 | name, 137 | email, 138 | company, 139 | phone, 140 | message, 141 | }) 142 | setIsSending(false) 143 | if (sendError) { 144 | setSentError(sendError) 145 | } else { 146 | setIsSent(true) 147 | } 148 | } 149 | 150 | return html` 214 | <${Page} 215 | title="Contact" 216 | subtitle="Get in touch with us" 217 | description="" 218 | sidebarImage=${prefixUriIfNeeded('/assets/default-contact.jpg')} 219 | > 220 |

Send me an email

221 |
222 |
223 | 224 | ${errorProperty === 'name' && 225 | html` ${formError} `} 226 | <${BaseInput} 227 | value=${name.value} 228 | name="name" 229 | placeholder="Jack Smith" 230 | onInput=${debouncedUpdateText} 231 | className="input-base ${errorProperty === 'name' 232 | ? 'input-error' 233 | : ''}" 234 | required 235 | /> 236 |
237 |
238 | 239 | ${errorProperty === 'email' && 240 | html` ${formError} `} 241 | <${BaseInput} 242 | value=${email.value} 243 | name="email" 244 | placeholder="example@mail.com" 245 | type="email" 246 | onInput=${debouncedUpdateEmail} 247 | className="input-base ${errorProperty === 'email' 248 | ? 'input-error' 249 | : ''}" 250 | required 251 | /> 252 |
253 |
254 | 255 | ${errorProperty === 'company' && 256 | html` ${formError} `} 257 | <${BaseInput} 258 | value=${company.value} 259 | name="company" 260 | placeholder="Example Corporation" 261 | onInput=${debouncedUpdateText} 262 | className="input-base ${errorProperty === 'company' 263 | ? 'input-error' 264 | : ''}" 265 | /> 266 |
267 |
268 | 269 | ${errorProperty === 'phone' && 270 | html` ${formError} `} 271 | <${BaseInput} 272 | value=${phone.value} 273 | name="phone" 274 | placeholder="+44778765439" 275 | onInput=${debouncedUpdateText} 276 | className="input-base ${errorProperty === 'phone' 277 | ? 'input-error' 278 | : ''}" 279 | /> 280 |
281 |
282 | 283 | ${errorProperty === 'message' && 284 | html` ${formError} `} 285 | <${BaseInput} 286 | value=${message.value} 287 | name="message" 288 | placeholder="Hello, let's chat!" 289 | Component="textarea" 290 | onInput=${debouncedUpdateText} 291 | className="input-base ${errorProperty === 'message' 292 | ? 'input-error' 293 | : ''}" 294 | required 295 | /> 296 |
297 |
298 | <${SubmitButton} 299 | isSending=${isSending} 300 | isSent=${isSent} 301 | formError=${formError} 302 | sentError=${sentError} 303 | sendMessage=${sendMessage} 304 | /> 305 |
306 | 315 | ` 316 | } 317 | 318 | const SubmitButton = ({ 319 | isSending, 320 | isSent, 321 | formError, 322 | sentError, 323 | sendMessage, 324 | }) => { 325 | const isDisabled = isSending || isSent || formError || sentError 326 | let message = ' Send message' 327 | let colorClass = '' 328 | if (formError) { 329 | message = 'Invalid form' 330 | } else if (isSending) { 331 | message = 'Sending...' 332 | } else if (sentError) { 333 | message = 'Failed to send message' 334 | colorClass = 'red' 335 | } else if (isSent) { 336 | message = 'Message sent!' 337 | colorClass = 'green' 338 | } 339 | return html`${sentError && html` ${sentError} `}` 352 | } 353 | -------------------------------------------------------------------------------- /app/routes/home.js: -------------------------------------------------------------------------------- 1 | import { html } from '../lib/htm-preact.js' 2 | 3 | import { Page } from '../components/layout/page.js' 4 | import prefixUriIfNeeded from '../utils/prefixUriIfNeeded.js' 5 | import { PostsAndCategories } from '../components/layout/postsAndCategories.js' 6 | 7 | export const Home = ({ state, dispatch }) => html` <${Page} 8 | title="Cats" 9 | subtitle="React Drive CMS Demo" 10 | description="Publish articles directly from Google Drive to your website." 11 | sidebarImage=${prefixUriIfNeeded('/assets/default-sidebar.jpg')} 12 | showLinks=${true} 13 | > 14 | <${PostsAndCategories} state=${state} dispatch=${dispatch} /> 15 | ` 16 | -------------------------------------------------------------------------------- /app/state.js: -------------------------------------------------------------------------------- 1 | import { getActiveItemId, getPageName } from './utils/path.js' 2 | 3 | export const initialState = { 4 | pageName: getPageName(), 5 | activeItemId: getActiveItemId(), 6 | } 7 | 8 | export const reducer = (state, action) => { 9 | switch (action.type) { 10 | case 'URI_CHANGE': 11 | return { 12 | ...state, 13 | pageName: action?.pageName, 14 | activeItemId: action?.activeItemId, 15 | } 16 | 17 | default: 18 | return state 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/styles/Spinner.js: -------------------------------------------------------------------------------- 1 | import { html } from '../lib/htm-preact.js' 2 | 3 | export const Spinner = ({ stroke = '#2f3d4c', width, height, rayon = 45 }) => { 4 | return html` 50 | 57 | 58 | ` 59 | } 60 | -------------------------------------------------------------------------------- /app/styles/blocks.js: -------------------------------------------------------------------------------- 1 | export const blocksStyles = `` 57 | .replace('', '') 59 | -------------------------------------------------------------------------------- /app/styles/buttons.js: -------------------------------------------------------------------------------- 1 | export const buttonsStyles = `` 47 | .replace('', '') 49 | -------------------------------------------------------------------------------- /app/styles/input.js: -------------------------------------------------------------------------------- 1 | export const inputStyles = `` 32 | .replace('', '') 34 | -------------------------------------------------------------------------------- /app/utils/avoidReload.js: -------------------------------------------------------------------------------- 1 | export const avoidReload = (event) => { 2 | event.preventDefault() 3 | event.stopPropagation() 4 | const href = event?.target?.closest('a')?.href 5 | if (href) { 6 | history.pushState({ url: href }, '', href) 7 | window.dispatchEvent(new PopStateEvent('popstate', {})) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/utils/capitalize.js: -------------------------------------------------------------------------------- 1 | export default function capitalize(string) { 2 | if (!string) { 3 | return '' 4 | } 5 | return string.charAt(0).toUpperCase() + string.slice(1) 6 | } 7 | -------------------------------------------------------------------------------- /app/utils/debounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * lodash (Custom Build) 3 | * Build: `lodash modularize exports="npm" -o ./` 4 | * Copyright jQuery Foundation and other contributors 5 | * Released under MIT license 6 | * Based on Underscore.js 1.8.3 7 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 8 | */ 9 | 10 | /** Used as the `TypeError` message for "Functions" methods. */ 11 | var FUNC_ERROR_TEXT = 'Expected a function' 12 | 13 | /** Used as references for various `Number` constants. */ 14 | var NAN = 0 / 0 15 | 16 | /** `Object#toString` result references. */ 17 | var symbolTag = '[object Symbol]' 18 | 19 | /** Used to match leading and trailing whitespace. */ 20 | var reTrim = /^\s+|\s+$/g 21 | 22 | /** Used to detect bad signed hexadecimal string values. */ 23 | var reIsBadHex = /^[-+]0x[0-9a-f]+$/i 24 | 25 | /** Used to detect binary string values. */ 26 | var reIsBinary = /^0b[01]+$/i 27 | 28 | /** Used to detect octal string values. */ 29 | var reIsOctal = /^0o[0-7]+$/i 30 | 31 | /** Built-in method references without a dependency on `root`. */ 32 | var freeParseInt = parseInt 33 | 34 | /** Detect free variable `global` from Node.js. */ 35 | var freeGlobal = 36 | typeof global == 'object' && global && global.Object === Object && global 37 | 38 | /** Detect free variable `self`. */ 39 | var freeSelf = typeof self == 'object' && self && self.Object === Object && self 40 | 41 | /** Used as a reference to the global object. */ 42 | var root = freeGlobal || freeSelf || Function('return this')() 43 | 44 | /** Used for built-in method references. */ 45 | var objectProto = Object.prototype 46 | 47 | /** 48 | * Used to resolve the 49 | * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring) 50 | * of values. 51 | */ 52 | var objectToString = objectProto.toString 53 | 54 | /* Built-in method references for those with the same name as other `lodash` methods. */ 55 | var nativeMax = Math.max, 56 | nativeMin = Math.min 57 | 58 | /** 59 | * Gets the timestamp of the number of milliseconds that have elapsed since 60 | * the Unix epoch (1 January 1970 00:00:00 UTC). 61 | * 62 | * @static 63 | * @memberOf _ 64 | * @since 2.4.0 65 | * @category Date 66 | * @returns {number} Returns the timestamp. 67 | * @example 68 | * 69 | * _.defer(function(stamp) { 70 | * console.log(_.now() - stamp); 71 | * }, _.now()); 72 | * // => Logs the number of milliseconds it took for the deferred invocation. 73 | */ 74 | var now = function () { 75 | return root.Date.now() 76 | } 77 | 78 | /** 79 | * Creates a debounced function that delays invoking `func` until after `wait` 80 | * milliseconds have elapsed since the last time the debounced function was 81 | * invoked. The debounced function comes with a `cancel` method to cancel 82 | * delayed `func` invocations and a `flush` method to immediately invoke them. 83 | * Provide `options` to indicate whether `func` should be invoked on the 84 | * leading and/or trailing edge of the `wait` timeout. The `func` is invoked 85 | * with the last arguments provided to the debounced function. Subsequent 86 | * calls to the debounced function return the result of the last `func` 87 | * invocation. 88 | * 89 | * **Note:** If `leading` and `trailing` options are `true`, `func` is 90 | * invoked on the trailing edge of the timeout only if the debounced function 91 | * is invoked more than once during the `wait` timeout. 92 | * 93 | * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred 94 | * until to the next tick, similar to `setTimeout` with a timeout of `0`. 95 | * 96 | * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) 97 | * for details over the differences between `_.debounce` and `_.throttle`. 98 | * 99 | * @static 100 | * @memberOf _ 101 | * @since 0.1.0 102 | * @category Function 103 | * @param {Function} func The function to debounce. 104 | * @param {number} [wait=0] The number of milliseconds to delay. 105 | * @param {Object} [options={}] The options object. 106 | * @param {boolean} [options.leading=false] 107 | * Specify invoking on the leading edge of the timeout. 108 | * @param {number} [options.maxWait] 109 | * The maximum time `func` is allowed to be delayed before it's invoked. 110 | * @param {boolean} [options.trailing=true] 111 | * Specify invoking on the trailing edge of the timeout. 112 | * @returns {Function} Returns the new debounced function. 113 | * @example 114 | * 115 | * // Avoid costly calculations while the window size is in flux. 116 | * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); 117 | * 118 | * // Invoke `sendMail` when clicked, debouncing subsequent calls. 119 | * jQuery(element).on('click', _.debounce(sendMail, 300, { 120 | * 'leading': true, 121 | * 'trailing': false 122 | * })); 123 | * 124 | * // Ensure `batchLog` is invoked once after 1 second of debounced calls. 125 | * var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); 126 | * var source = new EventSource('/stream'); 127 | * jQuery(source).on('message', debounced); 128 | * 129 | * // Cancel the trailing debounced invocation. 130 | * jQuery(window).on('popstate', debounced.cancel); 131 | */ 132 | function debounce(func, wait, options) { 133 | var lastArgs, 134 | lastThis, 135 | maxWait, 136 | result, 137 | timerId, 138 | lastCallTime, 139 | lastInvokeTime = 0, 140 | leading = false, 141 | maxing = false, 142 | trailing = true 143 | 144 | if (typeof func != 'function') { 145 | throw new TypeError(FUNC_ERROR_TEXT) 146 | } 147 | wait = toNumber(wait) || 0 148 | if (isObject(options)) { 149 | leading = !!options.leading 150 | maxing = 'maxWait' in options 151 | maxWait = maxing 152 | ? nativeMax(toNumber(options.maxWait) || 0, wait) 153 | : maxWait 154 | trailing = 'trailing' in options ? !!options.trailing : trailing 155 | } 156 | 157 | function invokeFunc(time) { 158 | var args = lastArgs, 159 | thisArg = lastThis 160 | 161 | lastArgs = lastThis = undefined 162 | lastInvokeTime = time 163 | result = func.apply(thisArg, args) 164 | return result 165 | } 166 | 167 | function leadingEdge(time) { 168 | // Reset any `maxWait` timer. 169 | lastInvokeTime = time 170 | // Start the timer for the trailing edge. 171 | timerId = setTimeout(timerExpired, wait) 172 | // Invoke the leading edge. 173 | return leading ? invokeFunc(time) : result 174 | } 175 | 176 | function remainingWait(time) { 177 | var timeSinceLastCall = time - lastCallTime, 178 | timeSinceLastInvoke = time - lastInvokeTime, 179 | result = wait - timeSinceLastCall 180 | 181 | return maxing 182 | ? nativeMin(result, maxWait - timeSinceLastInvoke) 183 | : result 184 | } 185 | 186 | function shouldInvoke(time) { 187 | var timeSinceLastCall = time - lastCallTime, 188 | timeSinceLastInvoke = time - lastInvokeTime 189 | 190 | // Either this is the first call, activity has stopped and we're at the 191 | // trailing edge, the system time has gone backwards and we're treating 192 | // it as the trailing edge, or we've hit the `maxWait` limit. 193 | return ( 194 | lastCallTime === undefined || 195 | timeSinceLastCall >= wait || 196 | timeSinceLastCall < 0 || 197 | (maxing && timeSinceLastInvoke >= maxWait) 198 | ) 199 | } 200 | 201 | function timerExpired() { 202 | var time = now() 203 | if (shouldInvoke(time)) { 204 | return trailingEdge(time) 205 | } 206 | // Restart the timer. 207 | timerId = setTimeout(timerExpired, remainingWait(time)) 208 | } 209 | 210 | function trailingEdge(time) { 211 | timerId = undefined 212 | 213 | // Only invoke if we have `lastArgs` which means `func` has been 214 | // debounced at least once. 215 | if (trailing && lastArgs) { 216 | return invokeFunc(time) 217 | } 218 | lastArgs = lastThis = undefined 219 | return result 220 | } 221 | 222 | function cancel() { 223 | if (timerId !== undefined) { 224 | clearTimeout(timerId) 225 | } 226 | lastInvokeTime = 0 227 | lastArgs = lastCallTime = lastThis = timerId = undefined 228 | } 229 | 230 | function flush() { 231 | return timerId === undefined ? result : trailingEdge(now()) 232 | } 233 | 234 | function debounced() { 235 | var time = now(), 236 | isInvoking = shouldInvoke(time) 237 | 238 | lastArgs = arguments 239 | lastThis = this 240 | lastCallTime = time 241 | 242 | if (isInvoking) { 243 | if (timerId === undefined) { 244 | return leadingEdge(lastCallTime) 245 | } 246 | if (maxing) { 247 | // Handle invocations in a tight loop. 248 | timerId = setTimeout(timerExpired, wait) 249 | return invokeFunc(lastCallTime) 250 | } 251 | } 252 | if (timerId === undefined) { 253 | timerId = setTimeout(timerExpired, wait) 254 | } 255 | return result 256 | } 257 | debounced.cancel = cancel 258 | debounced.flush = flush 259 | return debounced 260 | } 261 | 262 | /** 263 | * Checks if `value` is the 264 | * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types) 265 | * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) 266 | * 267 | * @static 268 | * @memberOf _ 269 | * @since 0.1.0 270 | * @category Lang 271 | * @param {*} value The value to check. 272 | * @returns {boolean} Returns `true` if `value` is an object, else `false`. 273 | * @example 274 | * 275 | * _.isObject({}); 276 | * // => true 277 | * 278 | * _.isObject([1, 2, 3]); 279 | * // => true 280 | * 281 | * _.isObject(_.noop); 282 | * // => true 283 | * 284 | * _.isObject(null); 285 | * // => false 286 | */ 287 | function isObject(value) { 288 | var type = typeof value 289 | return !!value && (type == 'object' || type == 'function') 290 | } 291 | 292 | /** 293 | * Checks if `value` is object-like. A value is object-like if it's not `null` 294 | * and has a `typeof` result of "object". 295 | * 296 | * @static 297 | * @memberOf _ 298 | * @since 4.0.0 299 | * @category Lang 300 | * @param {*} value The value to check. 301 | * @returns {boolean} Returns `true` if `value` is object-like, else `false`. 302 | * @example 303 | * 304 | * _.isObjectLike({}); 305 | * // => true 306 | * 307 | * _.isObjectLike([1, 2, 3]); 308 | * // => true 309 | * 310 | * _.isObjectLike(_.noop); 311 | * // => false 312 | * 313 | * _.isObjectLike(null); 314 | * // => false 315 | */ 316 | function isObjectLike(value) { 317 | return !!value && typeof value == 'object' 318 | } 319 | 320 | /** 321 | * Checks if `value` is classified as a `Symbol` primitive or object. 322 | * 323 | * @static 324 | * @memberOf _ 325 | * @since 4.0.0 326 | * @category Lang 327 | * @param {*} value The value to check. 328 | * @returns {boolean} Returns `true` if `value` is a symbol, else `false`. 329 | * @example 330 | * 331 | * _.isSymbol(Symbol.iterator); 332 | * // => true 333 | * 334 | * _.isSymbol('abc'); 335 | * // => false 336 | */ 337 | function isSymbol(value) { 338 | return ( 339 | typeof value == 'symbol' || 340 | (isObjectLike(value) && objectToString.call(value) == symbolTag) 341 | ) 342 | } 343 | 344 | /** 345 | * Converts `value` to a number. 346 | * 347 | * @static 348 | * @memberOf _ 349 | * @since 4.0.0 350 | * @category Lang 351 | * @param {*} value The value to process. 352 | * @returns {number} Returns the number. 353 | * @example 354 | * 355 | * _.toNumber(3.2); 356 | * // => 3.2 357 | * 358 | * _.toNumber(Number.MIN_VALUE); 359 | * // => 5e-324 360 | * 361 | * _.toNumber(Infinity); 362 | * // => Infinity 363 | * 364 | * _.toNumber('3.2'); 365 | * // => 3.2 366 | */ 367 | function toNumber(value) { 368 | if (typeof value == 'number') { 369 | return value 370 | } 371 | if (isSymbol(value)) { 372 | return NAN 373 | } 374 | if (isObject(value)) { 375 | var other = typeof value.valueOf == 'function' ? value.valueOf() : value 376 | value = isObject(other) ? other + '' : other 377 | } 378 | if (typeof value != 'string') { 379 | return value === 0 ? value : +value 380 | } 381 | value = value.replace(reTrim, '') 382 | var isBinary = reIsBinary.test(value) 383 | return isBinary || reIsOctal.test(value) 384 | ? freeParseInt(value.slice(2), isBinary ? 2 : 8) 385 | : reIsBadHex.test(value) 386 | ? NAN 387 | : +value 388 | } 389 | 390 | export default debounce 391 | -------------------------------------------------------------------------------- /app/utils/jsonpCall.js: -------------------------------------------------------------------------------- 1 | export default function jsonpCall(url, callback) { 2 | const handleJsonpResults = 3 | 'handleJsonpResults_' + 4 | Date.now() + 5 | '_' + 6 | parseInt(Math.random() * 10000) 7 | 8 | window[handleJsonpResults] = function (json) { 9 | callback(json) 10 | 11 | const script = document.getElementById(handleJsonpResults) 12 | document.getElementsByTagName('head')[0].removeChild(script) 13 | delete window[handleJsonpResults] 14 | } 15 | 16 | const serviceUrl = `${url}${ 17 | url.indexOf('?') > -1 ? '&' : '?' 18 | }callback=${handleJsonpResults}` 19 | 20 | const jsonpScript = document.createElement('script') 21 | jsonpScript.setAttribute('src', serviceUrl) 22 | jsonpScript.id = handleJsonpResults 23 | document.getElementsByTagName('head')[0].appendChild(jsonpScript) 24 | } 25 | -------------------------------------------------------------------------------- /app/utils/path.js: -------------------------------------------------------------------------------- 1 | export const getPathParts = function (uri) { 2 | const uriToSplit = uri ?? window.location.pathname 3 | return uriToSplit.split('/') 4 | } 5 | 6 | export const getPageName = function (uri) { 7 | const pathParts = getPathParts(uri) 8 | return window.location.origin.includes('localhost') 9 | ? pathParts[1] 10 | : pathParts[2] 11 | } 12 | 13 | export const getActiveItemId = function (uri) { 14 | const pathParts = getPathParts(uri) 15 | return window.location.origin.includes('localhost') 16 | ? pathParts[2] 17 | : pathParts[3] 18 | } 19 | -------------------------------------------------------------------------------- /app/utils/prefixUriIfNeeded.js: -------------------------------------------------------------------------------- 1 | const conf = window.appConf 2 | 3 | // this is to adapt URL to work on GitHub Pages 4 | export default function prefixUriIfNeeded(uri) { 5 | if (!uri) { 6 | return '' 7 | } 8 | const base = window.location.origin 9 | uri = uri.startsWith(base) ? uri : base + uri 10 | const root = `/${conf.root}` 11 | 12 | const shouldIncludeRoot = !base.includes('localhost') 13 | const path = uri.split(base)[1] 14 | const includesRoot = path.startsWith(root) 15 | if (!shouldIncludeRoot || includesRoot) { 16 | return uri 17 | } else { 18 | return base + root + path 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/utils/sleep.js: -------------------------------------------------------------------------------- 1 | export function sleep(ms) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)) 3 | } 4 | -------------------------------------------------------------------------------- /app/utils/to.js: -------------------------------------------------------------------------------- 1 | const nativeExceptions = [ 2 | EvalError, 3 | RangeError, 4 | ReferenceError, 5 | SyntaxError, 6 | TypeError, 7 | URIError, 8 | ].filter((except) => typeof except === 'function') 9 | 10 | /* Throw native errors. ref: https://bit.ly/2VsoCGE */ 11 | function throwNative(error) { 12 | for (const Exception of nativeExceptions) { 13 | if (error instanceof Exception) throw error 14 | } 15 | } 16 | 17 | /* Helper buddy for removing async/await try/catch litter */ 18 | export const to = function (promise, finallyFunc) { 19 | return promise 20 | .then((data) => { 21 | if (data instanceof Error) { 22 | throwNative(data) 23 | return [data] 24 | } 25 | return [undefined, data] 26 | }) 27 | .catch((error) => { 28 | throwNative(error) 29 | return [error] 30 | }) 31 | .finally(() => { 32 | if (finallyFunc && typeof finallyFunc === 'function') { 33 | finallyFunc() 34 | } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /app/utils/uuid.js: -------------------------------------------------------------------------------- 1 | let lut = [] 2 | for (let i = 0; i < 256; i++) { 3 | lut[i] = (i < 16 ? '0' : '') + i.toString(16) 4 | } 5 | 6 | function uuid() { 7 | let d0 = (Math.random() * 0xffffffff) | 0 8 | let d1 = (Math.random() * 0xffffffff) | 0 9 | let d2 = (Math.random() * 0xffffffff) | 0 10 | let d3 = (Math.random() * 0xffffffff) | 0 11 | return ( 12 | lut[d0 & 0xff] + 13 | lut[(d0 >> 8) & 0xff] + 14 | lut[(d0 >> 16) & 0xff] + 15 | lut[(d0 >> 24) & 0xff] + 16 | '-' + 17 | lut[d1 & 0xff] + 18 | lut[(d1 >> 8) & 0xff] + 19 | '-' + 20 | lut[((d1 >> 16) & 0x0f) | 0x40] + 21 | lut[(d1 >> 24) & 0xff] + 22 | '-' + 23 | lut[(d2 & 0x3f) | 0x80] + 24 | lut[(d2 >> 8) & 0xff] + 25 | '-' + 26 | lut[(d2 >> 16) & 0xff] + 27 | lut[(d2 >> 24) & 0xff] + 28 | lut[d3 & 0xff] + 29 | lut[(d3 >> 8) & 0xff] + 30 | lut[(d3 >> 16) & 0xff] + 31 | lut[(d3 >> 24) & 0xff] 32 | ) 33 | } 34 | 35 | export default uuid 36 | -------------------------------------------------------------------------------- /assets/default-about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-drive-cms/a30828d82b49b3e335d7e593b639b2ad5c9989b5/assets/default-about.jpg -------------------------------------------------------------------------------- /assets/default-contact.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-drive-cms/a30828d82b49b3e335d7e593b639b2ad5c9989b5/assets/default-contact.jpg -------------------------------------------------------------------------------- /assets/default-sidebar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-drive-cms/a30828d82b49b3e335d7e593b639b2ad5c9989b5/assets/default-sidebar.jpg -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-drive-cms/a30828d82b49b3e335d7e593b639b2ad5c9989b5/assets/favicon.ico -------------------------------------------------------------------------------- /assets/profile-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-drive-cms/a30828d82b49b3e335d7e593b639b2ad5c9989b5/assets/profile-1.jpg -------------------------------------------------------------------------------- /assets/react_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/misterfresh/react-drive-cms/a30828d82b49b3e335d7e593b639b2ad5c9989b5/assets/react_logo.png -------------------------------------------------------------------------------- /conf.js: -------------------------------------------------------------------------------- 1 | window.appConf = { 2 | author: 'React Drive CMS', 3 | dashboardId: '1-on_GfmvaEcOk7HcWfKb8B6KFRv166RkLN2YmDEtDn4', 4 | sendContactMessageUrlId: 5 | 'AKfycbyL4vW1UWs4mskuDjLoLmf1Hjan1rTLEca6i2Hi2H_4CtKUN84d', 6 | shortname: 'easydrivecms', 7 | root: 'react-drive-cms', 8 | ipInfoToken: '6efb7a808a4857', 9 | googleApiKey: 'AIzaSyBYOwC55SpcCZaG9d87UuHkxkQ8GRI_39M', 10 | } 11 | -------------------------------------------------------------------------------- /dev/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /dev/conf.js: -------------------------------------------------------------------------------- 1 | window.appConf = { 2 | author: 'React Drive CMS', 3 | dashboardId: '1-on_GfmvaEcOk7HcWfKb8B6KFRv166RkLN2YmDEtDn4', 4 | sendContactMessageUrlId: 5 | 'AKfycbyL4vW1UWs4mskuDjLoLmf1Hjan1rTLEca6i2Hi2H_4CtKUN84d', 6 | shortname: 'easydrivecms', 7 | ipInfoToken: '6efb7a808a4857', 8 | googleApiKey: 'AIzaSyBYOwC55SpcCZaG9d87UuHkxkQ8GRI_39M', 9 | } 10 | -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | React Drive CMS 11 | 12 | 13 | 14 | 15 | 37 | 38 | 39 | 84 | 85 | 86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /dev/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const port = process.argv[2] || 8080 6 | 7 | const index = fs.readFileSync( 8 | path.join(process.cwd(), 'dev', 'index.html'), 9 | 'utf8' 10 | ) 11 | const notFound = fs.readFileSync( 12 | path.join(process.cwd(), 'dev', '404.html'), 13 | 'utf8' 14 | ) 15 | 16 | http.createServer(function (req, res) { 17 | console.log(`${req.method} ${req.url}`) 18 | 19 | if (req.url === '/' || req.url.startsWith('/?p=')) { 20 | res.statusCode = 200 21 | res.setHeader('Content-type', 'text/html') 22 | res.end(index) 23 | return 24 | } 25 | // parse URL 26 | const parsedUrl = url.parse(req.url) 27 | // extract URL path 28 | let pathname = `.${parsedUrl.pathname}` 29 | // based on the URL path, extract the file extension. e.g. .js, .doc, ... 30 | const ext = path.parse(pathname).ext 31 | // maps file extention to MIME typere 32 | const map = { 33 | '.ico': 'image/x-icon', 34 | '.html': 'text/html', 35 | '.js_commonjs-proxy': 'text/javascript', 36 | '.js': 'text/javascript', 37 | '.json': 'application/json', 38 | '.css': 'text/css', 39 | '.png': 'image/png', 40 | '.jpg': 'image/jpeg', 41 | '.wav': 'audio/wav', 42 | '.mp3': 'audio/mpeg', 43 | '.svg': 'image/svg+xml', 44 | '.pdf': 'application/pdf', 45 | '.doc': 'application/msword', 46 | '.pbf': 'application/x-protobuf', 47 | } 48 | 49 | fs.exists(pathname, function (exist) { 50 | if (!exist) { 51 | if (!ext) { 52 | // if there is no extension, redirect to index with the uri as param 53 | res.writeHead(404, { 54 | 'Content-Type': 'text/html', 55 | Location: '/?p=' + req.url, 56 | }) 57 | res.end(notFound) 58 | return 59 | } else { 60 | // if the file is not found, return 404 61 | res.statusCode = 404 62 | res.end(`File ${pathname} not found!`) 63 | return 64 | } 65 | } 66 | 67 | // if is a directory search for index file matching the extention 68 | if (fs.statSync(pathname).isDirectory()) pathname += '/index' + ext 69 | 70 | // read file from file system 71 | fs.readFile(pathname, function (err, data) { 72 | if (err) { 73 | res.statusCode = 500 74 | res.end(`Error getting the file: ${err}.`) 75 | } else { 76 | // if the file is found, set Content-type and send data 77 | res.setHeader('Content-type', map[ext] || 'text/plain') 78 | res.end(data) 79 | } 80 | }) 81 | }) 82 | }).listen(parseInt(port)) 83 | 84 | console.log(`Server listening on port ${port}`) 85 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | React Drive CMS 10 | 11 | 12 | 13 | 14 | 36 | 37 | 38 | 84 | 85 | 86 |
87 | 88 | 89 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-drive-cms", 3 | "version": "3.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "prettier": { 8 | "version": "2.0.5", 9 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", 10 | "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "node ./dev/server.js", 4 | "format": "./node_modules/.bin/prettier --write --no-semi --tab-width 4 --single-quote \"{,!(node_modules|deps)/**/}*.js\"" 5 | }, 6 | "name": "react-drive-cms", 7 | "version": "4.2.7", 8 | "main": "app/app.js", 9 | "directories": { 10 | "doc": "docs" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "description": "", 15 | "dependencies": {}, 16 | "devDependencies": { 17 | "prettier": "^2.5.1" 18 | } 19 | } 20 | --------------------------------------------------------------------------------