Monorepo containing core modules of Zeichen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
7.9 KiB

  1. import * as React from 'react'
  2. import Head from 'next/head'
  3. import styled from 'styled-components'
  4. import { useRouter } from 'next/router'
  5. import Editor from '../components/Editor/Editor'
  6. import Navbar from '../components/Navbar/Navbar'
  7. import generateId from '../utilities/Id'
  8. import { formatDate } from '../utilities/Date'
  9. import * as Note from '../controllers/Note'
  10. import * as Folder from '../controllers/Folder'
  11. const Main = styled('main')({
  12. margin: '2rem 0',
  13. '@media (min-width: 1080px)': {
  14. paddingLeft: `${100 / 3}%`,
  15. boxSizing: 'border-box',
  16. },
  17. })
  18. const Container = styled('div')({
  19. width: '100%',
  20. margin: '0 auto',
  21. padding: '0 1rem',
  22. boxSizing: 'border-box',
  23. '@media (min-width: 720px)': {
  24. maxWidth: 720,
  25. },
  26. })
  27. const TitleInput = styled('input')({
  28. border: 0,
  29. background: 'transparent',
  30. padding: 0,
  31. display: 'block',
  32. width: '100%',
  33. font: 'inherit',
  34. fontSize: '3rem',
  35. fontWeight: 'bold',
  36. color: 'inherit',
  37. outline: 0,
  38. })
  39. const PostMeta = styled('small')({
  40. opacity: 0.5,
  41. height: '1.25rem',
  42. display: 'block',
  43. lineHeight: 1.25,
  44. })
  45. const PostPrimary = styled('div')({
  46. marginBottom: '2rem',
  47. })
  48. type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, }
  49. const Notes = ({ id: idProp }) => {
  50. // TODO remove extra state for ID
  51. const [id, setId, ] = React.useState(idProp)
  52. const [title, setTitle, ] = React.useState('')
  53. const [notes, setNotes, ] = React.useState(null)
  54. const [folders, setFolders, ] = React.useState(null)
  55. const stateRef = React.useRef<NoteInstance>({ id, title: '', updatedAt: new Date().toISOString(), })
  56. const timeoutRef = React.useRef<number>(null)
  57. const router = useRouter()
  58. React.useEffect(() => {
  59. Note.load({ setNotes })
  60. }, [])
  61. React.useEffect(() => {
  62. Folder.load({ setFolders })
  63. }, [])
  64. React.useEffect(() => {
  65. if (!Array.isArray(notes!)) {
  66. return
  67. }
  68. const theNote = notes.find(n => n.id === id)
  69. stateRef.current = theNote ? theNote : { id, title: '', updatedAt: new Date().toISOString(), }
  70. setTitle(stateRef.current.title)
  71. }, [id, notes])
  72. React.useEffect(() => {
  73. setId(idProp || generateId())
  74. }, [idProp])
  75. return (
  76. <React.Fragment>
  77. <Head>
  78. <title>{ idProp === undefined ? 'Notes | New Note' : `Notes | Edit Note - ${title.length > 0 ? title : '(untitled)'}`}</title>
  79. <link rel="icon" href="/favicon.ico" />
  80. </Head>
  81. <Navbar
  82. secondaryVisible={Boolean(router.query.navbar)}
  83. primaryItemsStart={[
  84. {
  85. id: 'sidebar',
  86. mobileOnly: true,
  87. active: Boolean(router.query.navbar),
  88. href: {
  89. pathname: router.pathname,
  90. query: {
  91. ...router.query,
  92. navbar: 'true',
  93. },
  94. },
  95. iconName: 'menu',
  96. title: 'Menu',
  97. },
  98. {
  99. id: 'folders',
  100. active: router.pathname.startsWith('/notes') && !Boolean(router.query.navbar),
  101. href: {
  102. pathname: '/notes',
  103. },
  104. iconName: 'note',
  105. title: 'Notes',
  106. },
  107. {
  108. id: 'search',
  109. href: {
  110. pathname: '/notes',
  111. query: {
  112. action: 'search',
  113. },
  114. },
  115. iconName: 'search',
  116. title: 'Search',
  117. },
  118. {
  119. id: 'binned',
  120. href: {
  121. pathname: '/notes',
  122. query: {
  123. status: 'binned',
  124. },
  125. },
  126. iconName: 'bin',
  127. title: 'Bin',
  128. },
  129. ]}
  130. primaryItemsEnd={[
  131. {
  132. id: 'user',
  133. href: {
  134. pathname: '/me',
  135. },
  136. iconName: 'user',
  137. title: 'User',
  138. },
  139. ]}
  140. secondaryItemsHeader={[
  141. {
  142. id: 'parent',
  143. href: {
  144. pathname: '/notes',
  145. query: {
  146. folder: '00000000-0000-0000-000000000000',
  147. navbar: router.query.navbar,
  148. },
  149. },
  150. iconName: 'back',
  151. title: 'Folder Name',
  152. // todo use history back
  153. },
  154. {
  155. id: 'note',
  156. href: {
  157. pathname: '/notes',
  158. query: {
  159. action: 'new',
  160. parentFolderId: '00000000-0000-0000-000000000000',
  161. },
  162. },
  163. iconName: 'new-note',
  164. title: 'Create Note',
  165. },
  166. {
  167. id: 'folder',
  168. href: {
  169. pathname: '/folders',
  170. query: {
  171. action: 'new',
  172. parentFolderId: '00000000-0000-0000-000000000000',
  173. },
  174. },
  175. iconName: 'new-folder',
  176. title: 'Create Child Folder',
  177. },
  178. {
  179. id: 'map',
  180. href: {
  181. pathname: '/notes',
  182. query: {
  183. action: 'view-map',
  184. parentFolderId: '00000000-0000-0000-000000000000',
  185. },
  186. },
  187. iconName: 'mind-map',
  188. title: 'View Folder Mind Map',
  189. },
  190. ]}
  191. secondaryItems={
  192. Array.isArray(notes!)
  193. ? notes.map(n => ({
  194. id: n.id,
  195. active: n.id === id,
  196. href: {
  197. pathname: '/notes/[id]',
  198. query: { id: n.id },
  199. },
  200. iconName: 'note',
  201. replace: true,
  202. title: n.title.trim(),
  203. subtitle: (
  204. <React.Fragment>
  205. {'Last updated '}
  206. <time
  207. dateTime={new Date(n.updatedAt).toISOString()}
  208. >
  209. {formatDate(new Date(n.updatedAt))}
  210. </time>
  211. </React.Fragment>
  212. ),
  213. actions: [
  214. {
  215. id: 'bin',
  216. iconName: 'bin',
  217. onClick: Note.remove({ setNotes, notes, router, })(n),
  218. }
  219. ],
  220. }))
  221. : []
  222. }
  223. />
  224. <Main>
  225. <Container>
  226. {
  227. Array.isArray(notes!)
  228. && (
  229. <React.Fragment>
  230. <PostPrimary>
  231. <TitleInput
  232. placeholder="Title"
  233. value={title}
  234. onChange={Note.updateTitle({
  235. stateRef,
  236. timeoutRef,
  237. router,
  238. id,
  239. setNotes,
  240. setTitle,
  241. })}
  242. />
  243. <PostMeta>
  244. {
  245. stateRef.current.updatedAt
  246. && router.query.id
  247. && (
  248. <time
  249. dateTime={new Date(stateRef.current.updatedAt).toISOString()}
  250. >
  251. Last updated {formatDate(new Date(stateRef.current.updatedAt))}
  252. </time>
  253. )
  254. }
  255. </PostMeta>
  256. </PostPrimary>
  257. <Editor
  258. autoFocus={false}
  259. key={id}
  260. content={stateRef.current ? stateRef.current.content : undefined}
  261. onChange={Note.updateContent({
  262. stateRef,
  263. timeoutRef,
  264. router,
  265. id,
  266. setNotes,
  267. })}
  268. placeholder="Start typing here..."
  269. />
  270. </React.Fragment>
  271. )
  272. }
  273. </Container>
  274. </Main>
  275. </React.Fragment>
  276. )
  277. }
  278. export const getServerSideProps = async ctx => {
  279. if (ctx.params) {
  280. return {
  281. props: {
  282. id: ctx.params?.id
  283. }
  284. }
  285. }
  286. return {
  287. props: {}
  288. }
  289. }
  290. export default Notes