Zeichen's app for both server and client.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 

316 lignes
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