├── .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 |
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 |
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 |
78 | Posts
79 |
80 |
87 | Categories
88 |
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 |
160 |
165 |
166 | ${activeArticle?.title}
167 |
168 | ${activeArticle?.subtitle}
169 |
173 | <${DisqusThread}
174 | articleId=${activeArticle.id}
175 | articleTitle=${activeArticle.title}
176 | />
177 |
178 | <${Footer}
179 | article=${activeArticle}
180 | articles=${articles}
181 | category=${category}
182 | menuVisible=${menuVisible}
183 | />
184 |
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 |
221 |
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`
345 | ${isSending &&
346 | html`<${Spinner}
347 | stroke="#eee"
348 | height=${18}
349 | width=${18}
350 | />`}${message} ${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 |
--------------------------------------------------------------------------------