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.

371 line
8.8 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 { Trash2, FilePlus, FolderPlus } 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 NoteLinkPrimary = styled('div')({
  68. display: 'block',
  69. })
  70. const NoteLinkTitle = styled('strong')({
  71. verticalAlign: 'middle',
  72. })
  73. const LinkContainer = styled('div')({
  74. position: 'relative',
  75. })
  76. const NoteActions = styled('div')({
  77. display: 'flex',
  78. position: 'absolute',
  79. alignItems: 'stretch',
  80. top: 0,
  81. right: 0,
  82. height: '100%',
  83. })
  84. const NoteAction = styled('button')({
  85. height: '100%',
  86. width: '4rem',
  87. background: 'transparent',
  88. border: 0,
  89. color: 'inherit',
  90. cursor: 'pointer',
  91. outline: 0,
  92. })
  93. const NoteLinkBackground = styled('span')({
  94. opacity: 0.125,
  95. backgroundColor: 'currentColor',
  96. top: 0,
  97. left: 0,
  98. width: '100%',
  99. height: '100%',
  100. position: 'absolute',
  101. })
  102. const NewIcon = styled(FilePlus)({
  103. verticalAlign: 'middle',
  104. marginRight: '0.5rem',
  105. })
  106. const NewFolderIcon = styled(FolderPlus)({
  107. verticalAlign: 'middle',
  108. marginRight: '0.5rem',
  109. })
  110. const BinIcon = styled(Trash2)({
  111. verticalAlign: 'middle',
  112. marginRight: '0.5rem',
  113. })
  114. type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, }
  115. const Notes = ({ id: idProp }) => {
  116. const [id, setId, ] = React.useState(idProp)
  117. const [title, setTitle, ] = React.useState('')
  118. const [notes, setNotes, ] = React.useState(null)
  119. const [folders, setFolders, ] = React.useState(null)
  120. const stateRef = React.useRef<NoteInstance>({ id, title: '', updatedAt: new Date().toISOString(), })
  121. const timeoutRef = React.useRef<number>(null)
  122. const router = useRouter()
  123. const autoSave = () => {
  124. if (timeoutRef.current !== null) {
  125. window.clearTimeout(timeoutRef.current)
  126. }
  127. timeoutRef.current = window.setTimeout(async () => {
  128. const newNote = await Storage.saveNote({
  129. ...stateRef.current,
  130. updatedAt: new Date().toISOString(),
  131. })
  132. if (router.query.id !== id) {
  133. await router.push(`/notes/${id}`, undefined, { shallow: true })
  134. }
  135. setNotes(oldNotes => {
  136. let notes
  137. if (oldNotes.some((a) => a.id === id)) {
  138. notes = oldNotes.map(n => n.id === id ? newNote : n)
  139. } else {
  140. notes = [newNote, ...oldNotes]
  141. }
  142. return notes.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
  143. })
  144. timeoutRef.current = null
  145. }, 3000)
  146. }
  147. const handleEditorChange = e => {
  148. stateRef.current.content = e
  149. autoSave()
  150. }
  151. const handleTitleChange = e => {
  152. stateRef.current.title = e.target.value
  153. setTitle(e.target.value)
  154. autoSave()
  155. }
  156. React.useEffect(() => {
  157. const loadNotes = async () => {
  158. const theNotes = await Storage.loadNotes()
  159. setNotes(theNotes)
  160. }
  161. loadNotes()
  162. }, [])
  163. React.useEffect(() => {
  164. const loadFolders = async () => {
  165. const theFolders = await Storage.loadFolders()
  166. setFolders(theFolders)
  167. }
  168. loadFolders()
  169. }, [])
  170. React.useEffect(() => {
  171. if (!Array.isArray(notes!)) {
  172. return
  173. }
  174. const theNote = notes.find(n => n.id === id)
  175. stateRef.current = theNote ? theNote : { id, title: '', updatedAt: new Date().toISOString(), }
  176. setTitle(stateRef.current.title)
  177. }, [id, notes])
  178. React.useEffect(() => {
  179. setId(idProp || generateId())
  180. }, [idProp])
  181. return (
  182. <React.Fragment>
  183. <Head>
  184. <title>{ idProp === undefined ? 'Notes | New Note' : `Notes | ${title.length > 0 ? title : '(untitled)'}`}</title>
  185. <link rel="icon" href="/favicon.ico" />
  186. </Head>
  187. <Navbar>
  188. <Link
  189. href={{
  190. pathname: '/profile',
  191. }}
  192. passHref
  193. >
  194. <NoteLink>
  195. <NavbarContainer>
  196. <NoteLinkPrimary>
  197. <NewFolderIcon />
  198. <NoteLinkTitle>
  199. Personal
  200. </NoteLinkTitle>
  201. </NoteLinkPrimary>
  202. </NavbarContainer>
  203. </NoteLink>
  204. </Link>
  205. <Link
  206. href={{
  207. pathname: '/folders/new',
  208. }}
  209. passHref
  210. >
  211. <NoteLink>
  212. <NavbarContainer>
  213. <NoteLinkPrimary>
  214. <NewFolderIcon />
  215. <NoteLinkTitle>
  216. Create Folder
  217. </NoteLinkTitle>
  218. </NoteLinkPrimary>
  219. </NavbarContainer>
  220. </NoteLink>
  221. </Link>
  222. <Link
  223. href={{
  224. pathname: '/notes',
  225. }}
  226. passHref
  227. >
  228. <NoteLink>
  229. <NavbarContainer>
  230. <NoteLinkPrimary>
  231. <NewIcon />
  232. <NoteLinkTitle>
  233. Create Note
  234. </NoteLinkTitle>
  235. </NoteLinkPrimary>
  236. </NavbarContainer>
  237. </NoteLink>
  238. </Link>
  239. {
  240. Array.isArray(notes!)
  241. && notes.map(n => (
  242. <LinkContainer
  243. key={n.id}
  244. >
  245. {
  246. n.id === id
  247. && (
  248. <NoteLinkBackground />
  249. )
  250. }
  251. <Link
  252. href={{
  253. pathname: '/notes/[id]',
  254. query: { id: n.id },
  255. }}
  256. passHref
  257. >
  258. <NoteLink>
  259. <NavbarContainer>
  260. <NoteLinkPrimary>
  261. <NoteLinkTitle
  262. style={{ opacity: n.title.length > 0 ? 1 : 0.5, }}
  263. >
  264. {n.title.length > 0 ? n.title : '(untitled)'}
  265. </NoteLinkTitle>
  266. </NoteLinkPrimary>
  267. {' '}
  268. <small>
  269. <time
  270. dateTime={new Date(n.updatedAt).toISOString()}
  271. >
  272. Last updated {formatDate(new Date(n.updatedAt))}
  273. </time>
  274. </small>
  275. </NavbarContainer>
  276. </NoteLink>
  277. </Link>
  278. <NoteActions>
  279. <NoteAction>
  280. <Trash2 />
  281. </NoteAction>
  282. </NoteActions>
  283. </LinkContainer>
  284. ))
  285. }
  286. <Link
  287. href={{
  288. pathname: '/bin',
  289. }}
  290. passHref
  291. >
  292. <NoteLink>
  293. <NavbarContainer>
  294. <NoteLinkPrimary>
  295. <BinIcon />
  296. <NoteLinkTitle>
  297. View Binned Notes
  298. </NoteLinkTitle>
  299. </NoteLinkPrimary>
  300. </NavbarContainer>
  301. </NoteLink>
  302. </Link>
  303. </Navbar>
  304. <Main>
  305. <Container>
  306. {
  307. Array.isArray(notes!)
  308. && (
  309. <React.Fragment>
  310. <TitleInput
  311. placeholder="Title"
  312. value={title}
  313. onChange={handleTitleChange}
  314. />
  315. <Editor
  316. autoFocus={false}
  317. key={id}
  318. content={stateRef.current ? stateRef.current.content : undefined}
  319. onChange={handleEditorChange}
  320. placeholder="Start typing here."
  321. />
  322. </React.Fragment>
  323. )
  324. }
  325. </Container>
  326. </Main>
  327. </React.Fragment>
  328. )
  329. }
  330. export const getServerSideProps = async ctx => {
  331. if (ctx.params) {
  332. return {
  333. props: {
  334. id: ctx.params?.id
  335. }
  336. }
  337. }
  338. return {
  339. props: {}
  340. }
  341. }
  342. export default Notes