add changes from v0.3

This commit is contained in:
JJ Kasper
2018-11-24 00:23:32 -06:00
parent 73f05ce4a3
commit 111cf2ed35
133 changed files with 10768 additions and 7443 deletions

View File

@@ -0,0 +1,59 @@
import React, { useEffect } from 'react'
import config from '../../util/pubConfig'
import MonokaiStyles from '../styles/codemirror/monokai'
import CodeMirrorStyles from '../styles/codemirror/codemirror'
let cm
let editor
let textareaRef
if (!config.ssr) {
require('codemirror/mode/markdown/markdown')
cm = require('codemirror')
}
export default function CodeMirror({
value,
style,
className,
options = {},
onChange = () => {},
onSubmit = () => {},
}) {
const handleChange = (cm, e) => onChange(cm.getValue())
const handleSubmit = (cm, e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
onSubmit()
}
}
useEffect(() => {
if (cm) {
if (!editor || editor.getTextArea() !== textareaRef) {
editor = cm.fromTextArea(textareaRef, options)
}
editor.on('change', handleChange)
editor.on('keydown', handleSubmit)
}
return () => {
if (editor) {
editor.off('change', handleChange)
editor.off('keydown', handleSubmit)
}
}
})
return (
<div {...{ className, style }}>
<textarea
{...{
defaultValue: value,
ref: el => (textareaRef = el),
}}
/>
<MonokaiStyles />
<CodeMirrorStyles />
</div>
)
}

104
src/client/comps/editDoc.js Normal file
View File

@@ -0,0 +1,104 @@
import React, { useState } from 'react'
import dynamic from 'next/dynamic'
import { connect } from 'react-redux'
import { updateDoc } from '../util/docHelpers'
import isOkDirPart from '../../util/isOkDirPart'
const Markdown = dynamic(() => import('react-markdown'))
const CodeMirror = dynamic(() => import('./codemirror'))
const dirError =
'contains an invalid character, must only have a-z, 0-9, -, _, and not start or end with a period or space'
function EditDoc({ cache, query }) {
const doc = cache[query.id] || {}
const [md, setMd] = useState(
doc.md || '### New document\n\nHeres some starting text'
)
const [dir, setDir] = useState(doc.dir || '')
const [name, setName] = useState(doc.name || '')
const [error, setError] = useState(null)
const [pending, setPending] = useState(false)
const handleSubmit = () => {
if (pending) return
let err
if (!name.trim()) {
err = 'Name is required'
} else if (!isOkDirPart(name)) {
err = `Name ${dirError}`
} else if (dir && dir.split('/').some(dirPart => !isOkDirPart(dirPart))) {
err = `Directory ${dirError}`
} else if (!md.trim()) {
err = 'Contents of markdown can not be empty'
}
if (err) return setError(err)
setError(null)
setPending(true)
updateDoc(query.id, md, name.trim(), dir).catch(err => {
setError(err.message)
setPending(false)
})
}
return (
<div className="container padded editDoc">
<div className="row">
<div className="column column-50">
<Markdown source={md} className="Markdown" />
</div>
<div className="column column-50">
<div className="row">
<div className="column column-60">
<input
type="text"
value={name}
maxLength={255}
placeholder="Document name"
onChange={e => setName(e.target.value)}
/>
</div>
<div className="column column">
<input
type="text"
value={dir}
placeholder="Directory (optional)"
onChange={e => setDir(e.target.value)}
/>
</div>
</div>
<div className="row">
<CodeMirror
value={md}
onChange={setMd}
onSubmit={handleSubmit}
style={{ width: '100%' }}
className="column wrapCodeMirror"
options={{
theme: 'monokai',
mode: 'markdown',
lineWrapping: true,
}}
/>
</div>
<div className="row" style={{ paddingTop: 15 }}>
<div className="column">
{error && <p className="float-left">{error}</p>}
<button className="float-right" onClick={handleSubmit}>
{pending ? 'Pending' : 'Submit'}
</button>
</div>
</div>
</div>
</div>
<style jsx global>{`
.wrapCodeMirror textarea {
margin-bottom: 0;
min-height: 300px;
}
`}</style>
</div>
)
}
export default connect(({ cache }) => ({ cache }))(EditDoc)

View File

@@ -0,0 +1,9 @@
import React from 'react'
export default function extLink({ children, ...props }) {
return (
<a rel="noopener noreferrer" target="_blank" {...props}>
{children}
</a>
)
}

View File

@@ -0,0 +1,172 @@
import React, { useEffect, useState } from 'react'
import config from '../../util/pubConfig'
import addBase from '../../util/addBase'
import loadDocs from '../util/loadDocs'
import { connect } from 'react-redux'
import Paginate from 'react-paginate'
import Router from 'next/router'
import Link from 'next/link'
let searchTimeout
let abortController
const { date, defDocsLimit, searchDelay, ssr } = config
const abort = () => {
abortController && abortController.abort()
}
function ListDocs({ docs, query }) {
const [offline, setOffline] = useState(ssr ? false : !navigator.onLine)
const curSort = query.sort || 'updated:-1'
const curPage = parseInt(query.page, 10) || 1
const pageCount = Math.ceil(docs.total / defDocsLimit)
const handleOffline = () => setOffline(true)
const handleOnline = () => setOffline(false)
useEffect(
() => {
abortController = loadDocs(query)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
abort()
clearTimeout(searchTimeout)
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
},
[query]
)
const updateUrl = query => {
clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => {
Router.push({ pathname: '/', query }, { pathname: addBase('/'), query })
}, searchDelay)
}
const handleField = e => {
const key = e.target.getAttribute('name')
updateUrl({
...query,
page: 1,
[key]: e.target.value.trim(),
})
}
const handlePage = ({ selected }) => {
if (curPage - 1 === selected) return
updateUrl({
...query,
page: selected + 1,
})
}
return (
<div className="container padded">
<form action={addBase('/')} method="GET">
<div className="row">
<input
type="text"
name="search"
maxLength={300}
className="search"
onChange={handleField}
defaultValue={query.search || ''}
placeholder="Search knowledge base..."
/>
<select
name="sort"
value={curSort}
onChange={handleField}
className="column column-25"
>
<option value="updated:-1">Updated (new to old)</option>
<option value="updated:1">Updated (old to new)</option>
<option value="created:-1">Created (new to old)</option>
<option value="created:1">Created (old to new)</option>
<option value="id:1">Path (A to Z)</option>
<option value="id:-1">Path (Z to A)</option>
</select>
</div>
<noscript>
<button type="submit" className="float-right">
Submit
</button>
</noscript>
</form>
{docs.error && <p>{docs.error} </p>}
<table>
<thead>
<tr>
<th>Doc{offline && ` (offline mode)`}</th>
<th>Modified</th>
</tr>
</thead>
<tbody>
{docs.results.map(doc => {
const docUrl = { pathname: '/doc', query: { id: doc.id } }
return (
<tr key={doc.id}>
<td>
<Link
href={docUrl}
as={{ ...docUrl, pathname: addBase('/doc') }}
>
<a>{doc.id}</a>
</Link>
</td>
<td>
{new Date(doc.updated).toLocaleDateString(
date.locale,
date.options
)}
</td>
</tr>
)
})}
</tbody>
</table>
{!docs.total && <p>No docs found...</p>}
{docs.total > defDocsLimit && (
<Paginate
previousLabel="Prev"
pageCount={pageCount}
marginPagesDisplayed={2}
activeClassName="active"
forcePage={curPage - 1}
onPageChange={handlePage}
containerClassName="paginate"
hrefBuilder={pg => addBase(`/?page=${pg}`)}
/>
)}
<style jsx>{`
.container {
max-width: 750px;
margin: 15px auto;
}
.row input {
margin-right: 20px;
}
td a {
display: block;
}
th:nth-of-type(2n),
td:nth-of-type(2n) {
text-align: right;
}
`}</style>
</div>
)
}
export default connect(({ docs }) => ({ docs }))(ListDocs)

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { connect } from 'react-redux'
import Login from '../forms/login'
function RequireUser({ children, user }) {
return user.id ? children : <Login doSetup={user.doSetup} />
}
export default connect(({ user }) => ({ user }))(RequireUser)

View File

@@ -0,0 +1,72 @@
import { useEffect } from 'react'
import Router from 'next/router'
import logout from '../util/logout'
import addBase from '../../util/addBase'
/* - keyboard shortcuts
g then h -> navigate home
g then n -> navigate to new doc
g then s -> navigate to settings
g then l -> logout
e (when on doc page) -> edit doc
/ (when on home page) -> focus search
ctrl/cmd + enter -> submit new doc (handled in CodeMirror component)
*/
const keyToUrl = {
72: '/',
78: '/new',
83: '/settings',
}
const keyToEl = {
69: { sel: '#edit', func: 'click' },
191: { sel: '.search', func: 'focus' },
}
const getKey = e => e.which || e.keyCode
let prevKey
const handleKeyDown = e => {
const tag = e.target.tagName
if (tag === 'INPUT' || tag === 'TEXTAREA') return
const key = getKey(e)
if (prevKey === 71) {
// prev key was g
switch (key) {
case 72:
case 78:
case 83: {
const url = keyToUrl[key]
Router.push(url, addBase(url))
break
}
case 76: {
// logout
setTimeout(logout, 1)
break
}
default:
break
}
}
switch (key) {
case 69:
case 191: {
const { sel, func } = keyToEl[key]
const el = document.querySelector(sel)
if (el) setTimeout(() => el[func](), 1)
break
}
default:
break
}
prevKey = key
}
export default function Shortcuts(props) {
useEffect(() => {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [])
return null
}