Zeichen's app for both server and client.
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.
 
 
 

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