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.

316 lines
8.2 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. closeHref={{
  83. pathname: router.pathname,
  84. query: Object
  85. .entries(router.query)
  86. .filter(([key]) => key !== 'navbar')
  87. .reduce(
  88. (theQuery, [key, value]) => ({
  89. ...theQuery,
  90. [key]: value,
  91. }),
  92. {}
  93. ),
  94. }}
  95. secondaryVisible={Boolean(router.query.navbar)}
  96. primaryItemsStart={[
  97. {
  98. id: 'sidebar',
  99. mobileOnly: true,
  100. active: Boolean(router.query.navbar),
  101. href: {
  102. pathname: router.pathname,
  103. query: {
  104. ...router.query,
  105. navbar: 'true',
  106. },
  107. },
  108. iconName: 'menu',
  109. title: 'Menu',
  110. },
  111. {
  112. id: 'folders',
  113. active: router.pathname.startsWith('/notes') && !Boolean(router.query.navbar),
  114. href: {
  115. pathname: '/notes',
  116. },
  117. iconName: 'note',
  118. title: 'Notes',
  119. },
  120. {
  121. id: 'search',
  122. href: {
  123. pathname: '/notes',
  124. query: {
  125. action: 'search',
  126. },
  127. },
  128. iconName: 'search',
  129. title: 'Search',
  130. },
  131. {
  132. id: 'binned',
  133. href: {
  134. pathname: '/notes',
  135. query: {
  136. status: 'binned',
  137. },
  138. },
  139. iconName: 'bin',
  140. title: 'Bin',
  141. },
  142. ]}
  143. primaryItemsEnd={[
  144. {
  145. id: 'user',
  146. href: {
  147. pathname: '/me',
  148. },
  149. iconName: 'user',
  150. title: 'User',
  151. },
  152. ]}
  153. secondaryItemsHeader={[
  154. {
  155. id: 'parent',
  156. href: {
  157. pathname: '/notes',
  158. query: {
  159. folder: '00000000-0000-0000-000000000000',
  160. navbar: router.query.navbar,
  161. },
  162. },
  163. iconName: 'back',
  164. title: 'Folder Name',
  165. // todo use history back
  166. },
  167. {
  168. id: 'note',
  169. href: {
  170. pathname: '/notes',
  171. query: {
  172. action: 'new',
  173. parentFolderId: '00000000-0000-0000-000000000000',
  174. },
  175. },
  176. iconName: 'new-note',
  177. title: 'Create Note',
  178. },
  179. {
  180. id: 'folder',
  181. href: {
  182. pathname: '/folders',
  183. query: {
  184. action: 'new',
  185. parentFolderId: '00000000-0000-0000-000000000000',
  186. },
  187. },
  188. iconName: 'new-folder',
  189. title: 'Create Child Folder',
  190. },
  191. {
  192. id: 'map',
  193. href: {
  194. pathname: '/notes',
  195. query: {
  196. action: 'view-map',
  197. parentFolderId: '00000000-0000-0000-000000000000',
  198. },
  199. },
  200. iconName: 'mind-map',
  201. title: 'View Folder Mind Map',
  202. },
  203. ]}
  204. secondaryItems={
  205. Array.isArray(notes!)
  206. ? notes.map(n => ({
  207. id: n.id,
  208. active: n.id === id,
  209. href: {
  210. pathname: '/notes/[id]',
  211. query: { id: n.id },
  212. },
  213. iconName: 'note',
  214. replace: true,
  215. title: n.title.trim(),
  216. subtitle: (
  217. <time
  218. dateTime={new Date(n.updatedAt).toISOString()}
  219. >
  220. {formatDate(new Date(n.updatedAt))}
  221. </time>
  222. ),
  223. actions: [
  224. {
  225. id: 'bin',
  226. iconName: 'bin',
  227. onClick: Note.remove({ setNotes, notes, router, })(n),
  228. }
  229. ],
  230. }))
  231. : []
  232. }
  233. />
  234. <Main>
  235. <Container>
  236. {
  237. Array.isArray(notes!)
  238. && (
  239. <React.Fragment>
  240. <PostPrimary>
  241. <TitleInput
  242. placeholder="Title"
  243. value={title}
  244. onChange={Note.updateTitle({
  245. stateRef,
  246. timeoutRef,
  247. router,
  248. id,
  249. setNotes,
  250. setTitle,
  251. })}
  252. />
  253. <PostMeta>
  254. {
  255. stateRef.current.updatedAt
  256. && router.query.id
  257. && (
  258. <time
  259. dateTime={new Date(stateRef.current.updatedAt).toISOString()}
  260. >
  261. Last updated {formatDate(new Date(stateRef.current.updatedAt))}
  262. </time>
  263. )
  264. }
  265. </PostMeta>
  266. </PostPrimary>
  267. <Editor
  268. autoFocus={false}
  269. key={id}
  270. content={stateRef.current ? stateRef.current.content : undefined}
  271. onChange={Note.updateContent({
  272. stateRef,
  273. timeoutRef,
  274. router,
  275. id,
  276. setNotes,
  277. })}
  278. placeholder="Start typing here..."
  279. />
  280. </React.Fragment>
  281. )
  282. }
  283. </Container>
  284. </Main>
  285. </React.Fragment>
  286. )
  287. }
  288. export const getServerSideProps = async ctx => {
  289. if (ctx.params) {
  290. return {
  291. props: {
  292. id: ctx.params?.id
  293. }
  294. }
  295. }
  296. return {
  297. props: {}
  298. }
  299. }
  300. export default Notes