Monorepo containing core modules of Zeichen.
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.

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