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.

300 lines
7.1 KiB

  1. import * as React from 'react'
  2. import Head from 'next/head'
  3. import styled from 'styled-components'
  4. import Editor from '../components/Editor/Editor'
  5. import * as Storage from '../services/Storage'
  6. import generateId from '../utilities/Id'
  7. import Link from 'next/link'
  8. import { formatDate } from '../utilities/Date'
  9. import { useRouter } from 'next/router'
  10. import { XCircle } from 'react-feather'
  11. const Navbar = styled('aside')({
  12. width: 360,
  13. height: '100%',
  14. position: 'fixed',
  15. top: 0,
  16. left: -360,
  17. backgroundColor: 'yellow',
  18. '@media (min-width: 1080px)': {
  19. width: `${100 / 3}%`,
  20. left: 0,
  21. },
  22. })
  23. const Main = styled('main')({
  24. margin: '2rem 0',
  25. '@media (min-width: 1080px)': {
  26. paddingLeft: `${100 / 3}%`,
  27. boxSizing: 'border-box',
  28. },
  29. })
  30. const Container = styled('div')({
  31. width: '100%',
  32. margin: '0 auto',
  33. padding: '0 1rem',
  34. boxSizing: 'border-box',
  35. '@media (min-width: 720px)': {
  36. maxWidth: 720,
  37. },
  38. })
  39. const NavbarContainer = styled('span')({
  40. display: 'block',
  41. width: '100%',
  42. margin: '0 0 0 auto',
  43. padding: '0 1rem',
  44. boxSizing: 'border-box',
  45. maxWidth: 360,
  46. })
  47. const TitleInput = styled('input')({
  48. border: 0,
  49. background: 'transparent',
  50. padding: 0,
  51. display: 'block',
  52. width: '100%',
  53. font: 'inherit',
  54. fontSize: '3rem',
  55. fontWeight: 'bold',
  56. outline: 0,
  57. marginBottom: '2rem',
  58. })
  59. const NoteLink = styled('a')({
  60. display: 'flex',
  61. textDecoration: 'none',
  62. color: 'inherit',
  63. height: '4rem',
  64. alignItems: 'center',
  65. position: 'relative',
  66. })
  67. const NoteLinkTitle = styled('strong')({
  68. display: 'block',
  69. })
  70. const LinkContainer = styled('div')({
  71. position: 'relative',
  72. })
  73. const NoteActions = styled('div')({
  74. display: 'flex',
  75. position: 'absolute',
  76. alignItems: 'stretch',
  77. top: 0,
  78. right: 0,
  79. height: '100%',
  80. })
  81. const NoteAction = styled('button')({
  82. height: '100%',
  83. width: '4rem',
  84. background: 'transparent',
  85. border: 0,
  86. color: 'inherit',
  87. cursor: 'pointer',
  88. outline: 0,
  89. })
  90. const NoteLinkBackground = styled('span')({
  91. opacity: 0.125,
  92. backgroundColor: 'currentColor',
  93. top: 0,
  94. left: 0,
  95. width: '100%',
  96. height: '100%',
  97. position: 'absolute',
  98. })
  99. type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, }
  100. const Notes = ({ id: idProp }) => {
  101. const [id, setId, ] = React.useState(idProp)
  102. const [title, setTitle, ] = React.useState('')
  103. const [notes, setNotes, ] = React.useState(null)
  104. const [folders, setFolders, ] = React.useState(null)
  105. const stateRef = React.useRef<NoteInstance>({ id, title: '', updatedAt: new Date().toISOString(), })
  106. const timeoutRef = React.useRef<number>(null)
  107. const router = useRouter()
  108. const autoSave = () => {
  109. if (timeoutRef.current !== null) {
  110. window.clearTimeout(timeoutRef.current)
  111. }
  112. timeoutRef.current = window.setTimeout(async () => {
  113. const newNote = await Storage.saveNote({
  114. ...stateRef.current,
  115. title,
  116. updatedAt: new Date().toISOString(),
  117. })
  118. if (router.query.id !== id) {
  119. await router.push(`/notes/${id}`, undefined, { shallow: true })
  120. }
  121. setNotes(oldNotes => {
  122. let notes
  123. if (oldNotes.some((a) => a.id === id)) {
  124. notes = oldNotes.map(n => n.id === id ? newNote : n)
  125. } else {
  126. notes = [newNote, ...oldNotes]
  127. }
  128. return notes.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
  129. })
  130. timeoutRef.current = null
  131. }, 3000)
  132. }
  133. const handleEditorChange = e => {
  134. stateRef.current.content = e
  135. autoSave()
  136. }
  137. const handleTitleChange = e => {
  138. setTitle(e.target.value)
  139. autoSave()
  140. }
  141. React.useEffect(() => {
  142. const loadNotes = async () => {
  143. const theNotes = await Storage.loadNotes()
  144. setNotes(theNotes)
  145. }
  146. loadNotes()
  147. }, [])
  148. React.useEffect(() => {
  149. const loadFolders = async () => {
  150. const theFolders = await Storage.loadFolders()
  151. setFolders(theFolders)
  152. }
  153. loadFolders()
  154. }, [])
  155. React.useEffect(() => {
  156. if (!Array.isArray(notes!)) {
  157. return
  158. }
  159. const theNote = notes.find(n => n.id === id)
  160. stateRef.current = theNote ? theNote : { id, title: '', updatedAt: new Date().toISOString(), }
  161. setTitle(stateRef.current.title)
  162. }, [id, notes])
  163. React.useEffect(() => {
  164. setId(idProp || generateId())
  165. }, [idProp])
  166. React.useEffect(() => {
  167. autoSave()
  168. }, [title])
  169. return (
  170. <React.Fragment>
  171. <Head>
  172. <title>{ idProp === undefined ? 'Notes | New Note' : `Notes | ${title.length > 0 ? title : '(untitled)'}`}</title>
  173. <link rel="icon" href="/favicon.ico" />
  174. </Head>
  175. <Navbar>
  176. <Link
  177. href={{
  178. pathname: '/notes',
  179. }}
  180. passHref
  181. >
  182. <NoteLink>
  183. <NavbarContainer>
  184. <NoteLinkTitle>
  185. New Note
  186. </NoteLinkTitle>
  187. </NavbarContainer>
  188. </NoteLink>
  189. </Link>
  190. {
  191. Array.isArray(notes!)
  192. && notes.map(n => (
  193. <LinkContainer
  194. key={n.id}
  195. >
  196. {
  197. n.id === id
  198. && (
  199. <NoteLinkBackground />
  200. )
  201. }
  202. <Link
  203. href={{
  204. pathname: '/notes/[id]',
  205. query: { id: n.id },
  206. }}
  207. passHref
  208. >
  209. <NoteLink>
  210. <NavbarContainer>
  211. <NoteLinkTitle
  212. style={{ opacity: n.title.length > 0 ? 1 : 0.5, }}
  213. >
  214. {n.title.length > 0 ? n.title : '(untitled)'}
  215. </NoteLinkTitle>
  216. {' '}
  217. <small>
  218. <time
  219. dateTime={new Date(n.updatedAt).toISOString()}
  220. >
  221. Last updated {formatDate(new Date(n.updatedAt))}
  222. </time>
  223. </small>
  224. </NavbarContainer>
  225. </NoteLink>
  226. </Link>
  227. <NoteActions>
  228. <NoteAction>
  229. <XCircle />
  230. </NoteAction>
  231. </NoteActions>
  232. </LinkContainer>
  233. ))
  234. }
  235. </Navbar>
  236. <Main>
  237. <Container>
  238. {
  239. Array.isArray(notes!)
  240. && (
  241. <React.Fragment>
  242. <TitleInput
  243. placeholder="Title"
  244. value={title}
  245. onChange={handleTitleChange}
  246. />
  247. <Editor
  248. autoFocus={false}
  249. key={id}
  250. content={stateRef.current ? stateRef.current.content : undefined}
  251. onChange={handleEditorChange}
  252. placeholder="Start typing here."
  253. />
  254. </React.Fragment>
  255. )
  256. }
  257. </Container>
  258. </Main>
  259. </React.Fragment>
  260. )
  261. }
  262. export const getServerSideProps = async ctx => {
  263. if (ctx.params) {
  264. return {
  265. props: {
  266. id: ctx.params?.id
  267. }
  268. }
  269. }
  270. return {
  271. props: {}
  272. }
  273. }
  274. export default Notes