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

@@ -1,50 +1,52 @@
import Head from 'next/head'
import '../src/styles/global'
import store from '../src/redux/store'
import { Provider } from 'react-redux'
import App, { Container } from 'next/app'
import { setUser, doLogin } from '../src/redux/actions/userAct'
import Head from 'next/head'
import { Provider } from 'react-redux'
import withRedux from '../src/client/hocs/withRedux'
import Header from '../src/client/layout/header'
import Footer from '../src/client/layout/footer'
import Shortcuts from '../src/client/comps/shortcuts'
import Milligram from '../src/client/styles/milligram'
import Roboto from '../src/client/styles/roboto'
import Global from '../src/client/styles/global'
import '../src/client/util/registerServiceWorker'
const ssr = typeof window === 'undefined'
export default class MyApp extends App {
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
let user = {}
let setup = false
if (ssr) {
user = ctx.req.user || {}
setup = ctx.req.doSetup || false
}
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return { Component, pageProps, user, setup }
}
componentWillMount() {
const { user, setup } = this.props
setUser({ ...user, setup })
if (!ssr && !user.email) {
const { jwt } = window.localStorage
if (jwt) doLogin(null, jwt, true)
}
return { Component, pageProps }
}
render() {
let { Component, pageProps } = this.props
const { Component, pageProps, reduxStore } = this.props
return (
<Provider store={store}>
<Provider store={reduxStore}>
<>
<Head>
<title>My Knowledge Base</title>
</Head>
<Container>
<Component {...pageProps} />
</Container>
<Header />
<div className="fill main">
<Container>
<Component {...pageProps} />
</Container>
</div>
<Footer />
<Shortcuts />
{/* style components */}
<Roboto />
<Milligram />
<Global />
</>
</Provider>
)
}
}
export default withRedux(MyApp)

View File

@@ -1,27 +1,13 @@
import getUrl from '../src/util/getUrl'
import { renderStaticOptimized } from 'glamor/server'
import Document, { Head, Main, NextScript } from 'next/document'
import addBase from '../src/util/addBase'
export default class MyDocument extends Document {
static async getInitialProps({ renderPage }) {
const page = renderPage()
const styles = renderStaticOptimized(() => page.html || page.errorHtml)
return { ...page, ...styles }
}
constructor(props) {
super(props)
const { __NEXT_DATA__, ids } = props
if (ids) {
__NEXT_DATA__.ids = this.props.ids
}
}
render() {
return (
<html>
<Head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
@@ -29,35 +15,36 @@ export default class MyDocument extends Document {
<link
rel="apple-touch-icon"
sizes="180x180"
href={getUrl('/apple-touch-icon.png')}
href={addBase('/apple-touch-icon.png')}
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href={getUrl('/favicon-32x32.png')}
href={addBase('/favicon-32x32.png')}
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href={getUrl('/favicon-16x16.png')}
href={addBase('/favicon-16x16.png')}
/>
<link rel="manifest" href={getUrl('/site.webmanifest')} />
<link rel="manifest" href={addBase('/site.webmanifest')} />
<link
rel="mask-icon"
href={getUrl('/safari-pinned-tab.svg')}
href={addBase('/safari-pinned-tab.svg')}
color="#00d1b2"
/>
<meta name="msapplication-TileColor" content="#202225" />
<meta name="theme-color" content="#202225" />
<style
dangerouslySetInnerHTML={{ __html: this.props.css }}
data-glamor
/>
<script
dangerouslySetInnerHTML={{
__html: 'window.kbConf=' + JSON.stringify(app.get('kbConf')),
__html:
'window.publicConfig=' +
JSON.stringify({
...global.publicConfig,
ssr: false,
}),
}}
/>
</Head>

39
pages/_error.js Normal file
View File

@@ -0,0 +1,39 @@
import React from 'react'
export default class Error extends React.Component {
static getInitialProps({ res, err }) {
let statusCode = null
if (res) statusCode = res.statusCode
else if (err) statusCode = err.statusCode
return { statusCode }
}
render() {
const { statusCode } = this.props
return (
<div className="fill">
<h4>
{(() => {
if (statusCode === 404) {
return (
<>
<span>404</span> | This page could not be found
</>
)
}
return statusCode ? `Error: ${statusCode}` : 'An error occurred...'
})()}
</h4>
<style jsx>{`
div {
display: flex;
align-items: center;
justify-content: center;
}
`}</style>
</div>
)
}
}

51
pages/doc.js Normal file
View File

@@ -0,0 +1,51 @@
import React from 'react'
import Link from 'next/link'
import dynamic from 'next/dynamic'
import { connect } from 'react-redux'
import addBase from '../src/util/addBase'
import loadDocs from '../src/client/util/loadDocs'
import { deleteDoc } from '../src/client/util/docHelpers'
import RequireUser from '../src/client/comps/requireUser'
const Markdown = dynamic(() => import('react-markdown'))
function Doc({ cache, query }) {
return (
<RequireUser>
<div className="container fill padded">
{(() => {
const { id, md } = cache[query.id] || {}
if (!id) return <p>Doc was not found...</p>
return (
<div>
<h5>
{`${id} - `}
<Link
href={{ pathname: '/edit', query }}
as={{ pathname: addBase('/edit'), query }}
>
<a id="edit">edit</a>
</Link>
<button
className="float-right"
onClick={() => deleteDoc(query.id)}
>
DELETE
</button>
</h5>
<Markdown source={md} className="Markdown" />
</div>
)
})()}
</div>
</RequireUser>
)
}
Doc.getInitialProps = async ({ query }) => {
await loadDocs(query, true)
return { query }
}
export default connect(({ cache }) => ({ cache }))(Doc)

View File

@@ -1,18 +1,19 @@
import React, { Component } from 'react'
import Page from '../src/components/Page'
import MngDoc from '../src/components/MngDoc'
import AddDoc from '../src/components/AddDoc'
import React from 'react'
import EditDoc from '../src/client/comps/editDoc'
import loadDocs from '../src/client/util/loadDocs'
import RequireUser from '../src/client/comps/requireUser'
class Edit extends Component {
render() {
const { found, doc } = this.props
if (!found)
return (
<Page>
<h3>Doc not found...</h3>
</Page>
)
return <MngDoc {...{ doc }} />
}
function Edit({ query }) {
return (
<RequireUser>
<EditDoc {...{ query }} />
</RequireUser>
)
}
export default AddDoc(Edit)
Edit.getInitialProps = async ({ query }) => {
await loadDocs(query, true)
return { query }
}
export default Edit

View File

@@ -1,179 +1,22 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import Router from 'next/router'
import Paginate from 'react-paginate'
import { format } from 'url'
import Page from '../src/components/Page'
import PaddedRow from '../src/components/PaddedRow'
import Spinner from '../src/components/Spinner'
import DocItem from '../src/components/DocItem'
import { $limit, getDocs, buildQ } from '../src/util/getDocs'
import getJwt from '../src/util/getJwt'
import getUrl from '../src/util/getUrl'
import mapUser from '../src/util/mapUser'
import React from 'react'
import config from '../src/util/pubConfig'
import loadDocs from '../src/client/util/loadDocs'
import ListDocs from '../src/client/comps/listDocs'
import RequireUser from '../src/client/comps/requireUser'
class Index extends Component {
state = {
$sort: 'updated:-1',
$search: '',
page: 1,
pending: false,
error: null,
total: 0,
docs: [],
}
static async getInitialProps({ req, query }) {
let page = 1,
$search = ''
if (query) {
page = query.page || page
$search = query.search || $search
}
const jwt = getJwt(req)
if (!jwt) return { total: 0, docs: [] }
const q = buildQ({ $search, $skip: page })
const data = await getDocs(q, req ? jwt : false)
return { ...data, page, $search }
}
static getDerivedStateFromProps(nextProps, prevState) {
let { docs, total, page, $search } = nextProps
if (
!prevState.didInit &&
(page !== prevState.page || $search !== prevState.$search)
) {
return { total, docs, page, $search, pending: false, didInit: true }
}
return null
}
pushQuery = query =>
Router.push(
{ pathname: '/', query },
format({ pathname: getUrl('/'), query })
)
updDocs = (time, doSearch) => {
clearTimeout(this.docsTime)
this.docsTime = setTimeout(async () => {
let { $sort, $search, page } = this.state
if (doSearch) {
const query = { search: $search }
if (!$search) delete query.search
this.pushQuery(query)
}
this.setState({ error: null })
this.docsTime = setTimeout(() => {
this.setState({ pending: true })
}, 125)
const q = buildQ({ $search, $sort, $skip: page })
const data = await getDocs(q)
clearTimeout(this.docsTime)
this.setState({ ...data, pending: false })
}, time || 275)
}
updQuery = e => {
this.setState({ [e.target.id]: e.target.value })
this.updDocs(0, e.target.id === '$search')
}
handlePage = ({ selected }) => {
const { $search } = this.state
const page = selected + 1
const query = {}
this.setState({ page })
if (page > 1) query.page = page
if ($search) query.search = $search
this.pushQuery(query)
this.updDocs(1)
}
componentDidMount() {
this.updDocs(1)
}
componentDidUpdate(prevProps) {
const { user, docs } = this.props
if (prevProps.user.email === user.email) return
if (user.email && docs.length === 0) this.updDocs(1)
}
render() {
const { $sort, $search, pending, error, docs, total, page } = this.state
const pages = Math.ceil(total / $limit)
return (
<Page>
<PaddedRow>
<input
type="text"
placeholder="Search knowledge base..."
maxLength={128}
value={$search}
className="search"
id="$search"
onChange={this.updQuery}
/>
</PaddedRow>
<PaddedRow>
<div className="inline" style={{ width: '100%' }}>
<h4 className="noMargin">Docs</h4>
<div className="float-right inline">
<label htmlFor="sort">Sort: </label>
<select
id="$sort"
value={$sort}
onChange={this.updQuery}
style={{ width: 150 }}
>
<option value="updated:-1">{'Updated (new -> old)'}</option>
<option value="updated:1">{'Updated (old -> new)'}</option>
<option value="created:-1">{'Created (new -> old)'}</option>
<option value="created:1">{'Created (old -> new)'}</option>
<option value="dirName:1">{'Name (A -> Z)'}</option>
<option value="dirName:-1">{'Name (Z -> A)'}</option>
</select>
</div>
</div>
</PaddedRow>
<PaddedRow>
{docs.length > 0 || error || pending ? null : <p>No docs found...</p>}
{!error ? null : <p>{error}</p>}
{!pending || error ? null : (
<Spinner style={{ margin: '25px auto 0' }} />
)}
{docs.length < 1 || pending || error ? null : (
<div>
<table>
<thead>
<tr>
<th>
Doc <span className="float-right">Modified</span>
</th>
</tr>
</thead>
<tbody>
{docs.map(doc => (
<DocItem {...doc} key={doc.id} />
))}
</tbody>
</table>
{pages < 2 ? null : (
<Paginate
pageCount={pages}
containerClassName="paginate"
activeClassName="active"
onPageChange={this.handlePage}
forcePage={page - 1}
/>
)}
</div>
)}
</PaddedRow>
</Page>
)
}
function Home(props) {
return (
<RequireUser>
<ListDocs {...props} />
</RequireUser>
)
}
export default connect(mapUser)(Index)
Home.getInitialProps = async ctx => {
if (config.ssr) {
await loadDocs(ctx.query || {})
}
return { query: ctx.query }
}
export default Home

View File

@@ -1,65 +0,0 @@
import React, { Component } from 'react'
import Link from 'next/link'
import Router from 'next/router'
import fetch from 'isomorphic-unfetch'
import Page from '../src/components/Page'
import Markdown from '../src/components/Markdown'
import AddDoc from '../src/components/AddDoc'
import getUrl from '../src/util/getUrl'
import getJwt from '../src/util/getJwt'
class k extends Component {
delete = async () => {
const sure = window.confirm(
'Are you sure you want to delete this doc? This can not be undone.'
)
if (!sure) return
const del = await fetch(getUrl('docs/' + this.props.id), {
headers: { Authorization: getJwt() },
method: 'DELETE',
}).catch(({ message }) => ({ ok: false, message }))
if (del.ok) Router.push('/', getUrl('/'))
else {
if (!del.message) {
const data = await del.json()
del.message = data.message
}
window.alert(`Could not delete doc, ${del.message}`)
}
}
render() {
const { found, id, doc } = this.props
if (!found)
return (
<Page>
<h3>Doc not found...</h3>
</Page>
)
return (
<Page>
<h5 style={{ marginBottom: '1rem' }}>
{doc.dir}
{doc.dir.length > 0 ? '/' : ''}
{doc.name}
{' - '}
<Link
as={getUrl('edit/' + id)}
href={{ pathname: '/edit', query: { id } }}
>
<a id="edit">edit</a>
</Link>
<button
className="float-right"
onClick={this.delete}
style={{ margin: '5px 0 0' }}
>
Delete
</button>
</h5>
<Markdown source={doc.md} className="Markdown" />
</Page>
)
}
}
export default AddDoc(k)

View File

@@ -1,2 +1,15 @@
import MngDoc from '../src/components/MngDoc'
export default MngDoc
import React from 'react'
import EditDoc from '../src/client/comps/editDoc'
import RequireUser from '../src/client/comps/requireUser'
function New({ query }) {
return (
<RequireUser>
<EditDoc {...{ query }} />
</RequireUser>
)
}
New.getInitialProps = async ({ query }) => ({ query })
export default New

23
pages/offline.js Normal file
View File

@@ -0,0 +1,23 @@
import { useEffect } from 'react'
import Router from 'next/router'
import addBase from '../src/util/addBase'
export default function Offline() {
// force next to render correct route on mount
useEffect(() => {
const { pathname, search } = window.location
let origRoute = pathname + search
let curRoute = origRoute.split(addBase('/'))
curRoute.splice(0, 1)
curRoute = '/' + curRoute.join(addBase('/'))
if (curRoute === '/offline' && navigator.onLine) {
curRoute = '/'
origRoute = '/'
}
Router.push(curRoute, origRoute)
}, [])
return null
}

View File

@@ -1,114 +1,128 @@
import React, { Component } from 'react'
import React, { useState } from 'react'
import { connect } from 'react-redux'
import fetch from 'isomorphic-unfetch'
import Page from '../src/components/Page'
import PaddedRow from '../src/components/PaddedRow'
import Spinner from '../src/components/Spinner'
import updStateFromId from '../src/util/updStateFromId'
import mapUser from '../src/util/mapUser'
import getUrl from '../src/util/getUrl'
import getJwt from '../src/util/getJwt'
import addBase from '../src/util/addBase'
import getHeaders from '../src/client/util/getHeaders'
import RequireUser from '../src/client/comps/requireUser'
class Settings extends Component {
state = {
pending: false,
passErr: null,
curPass: '',
newPass: '',
confPass: '',
}
updVal = updStateFromId.bind(this)
submit = async e => {
e.preventDefault()
const { pending, curPass, newPass, confPass } = this.state
const { email, _id } = this.props.user
function Settings({ user }) {
const [data, setData] = useState({
current: '',
confirm: '',
new: '',
})
const [pending, setPending] = useState(false)
const [error, setError] = useState(null)
const handleSubmit = () => {
if (pending) return
const doErr = passErr => this.setState({ pending: false, passErr })
const vals = {
'Current password': curPass,
'New password': newPass,
'Confirm new password': confPass,
}
const keys = Object.keys(vals)
for (let i = 0; i < keys.length; i++) {
let key = keys[i],
val = vals[key]
if (val.length === 0) return doErr(`${key} is required`)
}
if (newPass !== confPass) return doErr("New passwords don't match")
let err
Object.keys(data).forEach(k => (data[k] = data[k].trim()))
if (!data.current) err = 'current pass is required'
else if (!data.new) err = 'new pass is required'
else if (!data.confirm) err = 'confirm pass is required'
else if (data.new !== data.confirm) err = 'new password must match confirm'
this.setState({ passErr: null, pending: true })
const updRes = await fetch(getUrl('users/' + _id), {
if (err) return setError(err)
setError(null)
setPending(true)
fetch(addBase('/user'), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: getJwt() },
body: JSON.stringify({ email, password: curPass, newPassword: newPass }),
}).catch(doErr)
if (updRes.ok) {
this.setState({
curPass: '',
newPass: '',
confPass: '',
passErr: 'Password updated successfully',
pending: false,
headers: {
...getHeaders(),
'content-type': 'application/json',
},
body: JSON.stringify({
username: user.username,
password: data.current,
newPassword: data.new,
}),
})
.then(async res => {
const { status, ...data } = await res.json()
if (status === 'ok') {
setPending(false)
setError('Password updated')
return setData({
current: '',
confirm: '',
new: '',
})
}
throw new Error(data.message)
})
.catch(err => {
setPending(false)
setError(err.message || 'An error occurred updating password')
})
} else {
let message = 'failed to update password'
try {
const data = await updRes.json()
message = data.message || message
} catch (err) {
doErr(err.message)
}
doErr(message)
}
}
render() {
const { pending, passErr, curPass, newPass, confPass } = this.state
const handleChange = e => {
data[e.target.id] = e.target.value
setData(data)
}
return (
<Page>
<PaddedRow amount={25}>
<h3>Account settings</h3>
<hr />
<form noValidate style={{ padding: '0 0 45px' }}>
<h4>Change password</h4>
<fieldset>
<label htmlFor="curPass">Current Password</label>
<input
type="password"
id="curPass"
onChange={this.updVal}
placeholder="Current super secret password..."
value={curPass}
/>
<label htmlFor="newPass">New Password</label>
<input
type="password"
id="newPass"
onChange={this.updVal}
placeholder="New super secret password..."
value={newPass}
/>
<label htmlFor="confPass">Confirm New Password</label>
<input
type="password"
id="confPass"
onChange={this.updVal}
placeholder="Confirm new super secret password..."
value={confPass}
/>
</fieldset>
<button
onClick={this.submit}
className={'float-right' + (pending ? ' disabled' : '')}
>
{pending ? <Spinner /> : 'Submit'}
</button>
{!passErr ? null : <p>{passErr}</p>}
</form>
</PaddedRow>
</Page>
)
}
return (
<RequireUser>
<div className="container padded fill">
<h3>Account settings</h3>
<hr />
<h4>Change password</h4>
<label htmlFor="current">Current Password</label>
<input
id="current"
type="password"
placeholder="Current super secret password"
value={data.current}
onChange={handleChange}
/>
<label htmlFor="new">New Password</label>
<input
id="new"
type="password"
placeholder="New super secret password"
value={data.new}
onChange={handleChange}
/>
<label htmlFor="confirm">Confirm New Password</label>
<input
id="confirm"
type="password"
value={data.confirm}
onChange={handleChange}
placeholder="Confirm its not too secret you forgot"
/>
<div>
{error && <p className="float-left">{error}</p>}
<button className="float-right" onClick={handleSubmit}>
Submit
</button>
</div>
<style jsx>{`
.container {
width: 100%;
margin: auto;
padding: 10px;
max-width: 550px;
}
h3 {
margin-top: 15px;
margin-bottom: 0 !important;
}
button {
margin-top: 5px;
}
`}</style>
</div>
</RequireUser>
)
}
export default connect(mapUser)(Settings)
export default connect(({ user }) => ({ user }))(Settings)