Monorepo containing core modules of Zeichen.
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

495 Zeilen
13 KiB

  1. import * as React from 'react'
  2. import Head from 'next/head'
  3. import styled from 'styled-components'
  4. import { Trash2, FilePlus, FolderPlus, FileText, GitBranch, User } from 'react-feather'
  5. import { useRouter } from 'next/router'
  6. import Editor from '../components/Editor/Editor'
  7. import generateId from '../utilities/Id'
  8. import Link from 'next/link'
  9. import { formatDate } from '../utilities/Date'
  10. import * as Note from '../controllers/Note'
  11. import * as Folder from '../controllers/Folder'
  12. const Navbar = styled('aside')({
  13. width: 360,
  14. height: '100%',
  15. position: 'fixed',
  16. top: 0,
  17. left: -360,
  18. backgroundColor: 'var(--color-fg)',
  19. color: 'var(--color-bg)',
  20. '@media (min-width: 1080px)': {
  21. width: `${100 / 3}%`,
  22. left: 0,
  23. },
  24. })
  25. const PrimaryNavItems = styled('nav')({
  26. height: '100%',
  27. display: 'flex',
  28. flexDirection: 'column',
  29. justifyContent: 'space-between',
  30. alignItems: 'stretch',
  31. '@media (min-width: 1080px)': {
  32. width: '4rem',
  33. },
  34. })
  35. const SecondaryNavItems = styled('nav')({
  36. height: '100%',
  37. position: 'relative',
  38. backgroundColor: 'var(--color-bg)',
  39. '::before': {
  40. content: "''",
  41. display: 'block',
  42. top: 0,
  43. left: 0,
  44. width: '100%',
  45. height: '100%',
  46. position: 'absolute',
  47. backgroundColor: 'black',
  48. opacity: 0.03125,
  49. },
  50. '@media (min-width: 1080px)': {
  51. flex: 'auto',
  52. },
  53. })
  54. const SecondaryNavItemsOverflow = styled('div')({
  55. overflow: 'auto',
  56. width: '100%',
  57. height: '100%',
  58. position: 'relative',
  59. })
  60. const Main = styled('main')({
  61. margin: '2rem 0',
  62. '@media (min-width: 1080px)': {
  63. paddingLeft: `${100 / 3}%`,
  64. boxSizing: 'border-box',
  65. },
  66. })
  67. const Container = styled('div')({
  68. width: '100%',
  69. margin: '0 auto',
  70. padding: '0 1rem',
  71. boxSizing: 'border-box',
  72. '@media (min-width: 720px)': {
  73. maxWidth: 720,
  74. },
  75. })
  76. const NavbarItems = styled('div')({
  77. display: 'flex',
  78. width: '100%',
  79. height: '100%',
  80. })
  81. const NavbarContainer = styled('div')({
  82. display: 'block',
  83. width: '100%',
  84. height: '100%',
  85. margin: '0 0 0 auto',
  86. boxSizing: 'border-box',
  87. maxWidth: 360,
  88. })
  89. const TitleInput = styled('input')({
  90. border: 0,
  91. background: 'transparent',
  92. padding: 0,
  93. display: 'block',
  94. width: '100%',
  95. font: 'inherit',
  96. fontSize: '3rem',
  97. fontWeight: 'bold',
  98. color: 'inherit',
  99. outline: 0,
  100. })
  101. const NoteLink = styled('a')({
  102. display: 'flex',
  103. textDecoration: 'none',
  104. color: 'inherit',
  105. height: '4rem',
  106. alignItems: 'center',
  107. position: 'relative',
  108. })
  109. const NoteLinkPrimary = styled('div')({
  110. display: 'block',
  111. })
  112. const NoteLinkTitle = styled('strong')({
  113. verticalAlign: 'middle',
  114. })
  115. const LinkContainer = styled('div')({
  116. position: 'relative',
  117. color: 'var(--color-primary, blue)',
  118. })
  119. const NoteActions = styled('div')({
  120. display: 'flex',
  121. position: 'absolute',
  122. alignItems: 'stretch',
  123. top: 0,
  124. right: 0,
  125. height: '100%',
  126. })
  127. const NoteAction = styled('button')({
  128. height: '100%',
  129. width: '4rem',
  130. background: 'transparent',
  131. border: 0,
  132. color: 'inherit',
  133. cursor: 'pointer',
  134. outline: 0,
  135. })
  136. const NoteLinkBackground = styled('span')({
  137. '::before': {
  138. content: "''",
  139. position: 'absolute',
  140. top: 0,
  141. left: 0,
  142. width: '0.25rem',
  143. height: '100%',
  144. display: 'block',
  145. backgroundColor: 'currentColor',
  146. },
  147. '::after': {
  148. content: "''",
  149. opacity: 0.125,
  150. backgroundColor: 'currentColor',
  151. top: 0,
  152. left: 0,
  153. width: '100%',
  154. height: '100%',
  155. position: 'absolute',
  156. },
  157. })
  158. const NewIcon = styled(FilePlus)({
  159. verticalAlign: 'middle',
  160. marginRight: '0.5rem',
  161. })
  162. const NewFolderIcon = styled(FolderPlus)({
  163. verticalAlign: 'middle',
  164. marginRight: '0.5rem',
  165. })
  166. const BinIcon = styled(Trash2)({
  167. verticalAlign: 'middle',
  168. marginRight: '0.5rem',
  169. })
  170. const NavbarItemContent = styled('span')({
  171. padding: '0 1rem',
  172. boxSizing: 'border-box',
  173. })
  174. const PrimaryNavItem = styled('a')({
  175. width: '4rem',
  176. height: '4rem',
  177. display: 'grid',
  178. placeContent: 'center',
  179. color: 'inherit',
  180. })
  181. const PostMeta = styled('small')({
  182. opacity: 0.5,
  183. height: '1.25rem',
  184. display: 'block',
  185. lineHeight: 1.25,
  186. })
  187. const PostPrimary = styled('div')({
  188. marginBottom: '2rem',
  189. })
  190. type NoteInstance = { id: string, title: string, content?: object, updatedAt: string, }
  191. const Notes = ({ id: idProp }) => {
  192. // TODO remove extra state for ID
  193. const [id, setId, ] = React.useState(idProp)
  194. const [title, setTitle, ] = React.useState('')
  195. const [notes, setNotes, ] = React.useState(null)
  196. const [folders, setFolders, ] = React.useState(null)
  197. const stateRef = React.useRef<NoteInstance>({ id, title: '', updatedAt: new Date().toISOString(), })
  198. const timeoutRef = React.useRef<number>(null)
  199. const router = useRouter()
  200. React.useEffect(() => {
  201. Note.load({ setNotes })
  202. }, [])
  203. React.useEffect(() => {
  204. Folder.loadFolders({ setFolders })
  205. }, [])
  206. React.useEffect(() => {
  207. if (!Array.isArray(notes!)) {
  208. return
  209. }
  210. const theNote = notes.find(n => n.id === id)
  211. stateRef.current = theNote ? theNote : { id, title: '', updatedAt: new Date().toISOString(), }
  212. setTitle(stateRef.current.title)
  213. }, [id, notes])
  214. React.useEffect(() => {
  215. setId(idProp || generateId())
  216. }, [idProp])
  217. return (
  218. <React.Fragment>
  219. <Head>
  220. <title>{ idProp === undefined ? 'Notes | New Note' : `Notes | Edit Note - ${title.length > 0 ? title : '(untitled)'}`}</title>
  221. <link rel="icon" href="/favicon.ico" />
  222. </Head>
  223. <Navbar>
  224. <NavbarContainer>
  225. <NavbarItems>
  226. <PrimaryNavItems>
  227. <div>
  228. <Link
  229. href={{
  230. pathname: '/notes',
  231. }}
  232. passHref
  233. >
  234. <PrimaryNavItem>
  235. <FileText />
  236. </PrimaryNavItem>
  237. </Link>
  238. <Link
  239. href={{
  240. pathname: '/graph',
  241. }}
  242. passHref
  243. >
  244. <PrimaryNavItem>
  245. <GitBranch />
  246. </PrimaryNavItem>
  247. </Link>
  248. </div>
  249. <div>
  250. <Link
  251. href={{
  252. pathname: '/me',
  253. }}
  254. passHref
  255. >
  256. <PrimaryNavItem>
  257. <User />
  258. </PrimaryNavItem>
  259. </Link>
  260. </div>
  261. </PrimaryNavItems>
  262. <SecondaryNavItems>
  263. <SecondaryNavItemsOverflow>
  264. <LinkContainer>
  265. <Link
  266. href={{
  267. pathname: '/profile',
  268. }}
  269. passHref
  270. >
  271. <NoteLink>
  272. <NavbarItemContent>
  273. <NoteLinkPrimary>
  274. <NewFolderIcon />
  275. <NoteLinkTitle>
  276. Personal
  277. </NoteLinkTitle>
  278. </NoteLinkPrimary>
  279. </NavbarItemContent>
  280. </NoteLink>
  281. </Link>
  282. </LinkContainer>
  283. <LinkContainer>
  284. <Link
  285. href={{
  286. pathname: '/folders/new',
  287. }}
  288. passHref
  289. >
  290. <NoteLink>
  291. <NavbarItemContent>
  292. <NoteLinkPrimary>
  293. <NewFolderIcon />
  294. <NoteLinkTitle>
  295. Create Folder
  296. </NoteLinkTitle>
  297. </NoteLinkPrimary>
  298. </NavbarItemContent>
  299. </NoteLink>
  300. </Link>
  301. </LinkContainer>
  302. <LinkContainer>
  303. <Link
  304. href={{
  305. pathname: '/notes',
  306. }}
  307. passHref
  308. >
  309. <NoteLink>
  310. <NavbarItemContent>
  311. <NoteLinkPrimary>
  312. <NewIcon />
  313. <NoteLinkTitle>
  314. Create Note
  315. </NoteLinkTitle>
  316. </NoteLinkPrimary>
  317. </NavbarItemContent>
  318. </NoteLink>
  319. </Link>
  320. </LinkContainer>
  321. {
  322. Array.isArray(notes!)
  323. && notes.map(n => (
  324. <LinkContainer
  325. key={n.id}
  326. >
  327. {
  328. n.id === id
  329. && (
  330. <NoteLinkBackground />
  331. )
  332. }
  333. <Link
  334. href={{
  335. pathname: '/notes/[id]',
  336. query: { id: n.id },
  337. }}
  338. replace
  339. passHref
  340. >
  341. <NoteLink>
  342. <NavbarItemContent>
  343. <NoteLinkPrimary>
  344. <NoteLinkTitle
  345. style={{ opacity: n.title.length > 0 ? 1 : 0.5, }}
  346. >
  347. {n.title.length > 0 ? n.title : '(untitled)'}
  348. </NoteLinkTitle>
  349. </NoteLinkPrimary>
  350. {' '}
  351. <PostMeta>
  352. <time
  353. dateTime={new Date(n.updatedAt).toISOString()}
  354. >
  355. Last updated {formatDate(new Date(n.updatedAt))}
  356. </time>
  357. </PostMeta>
  358. </NavbarItemContent>
  359. </NoteLink>
  360. </Link>
  361. <NoteActions>
  362. <NoteAction
  363. onClick={Note.remove({ setNotes, notes, router, })(n)}
  364. >
  365. <Trash2 />
  366. </NoteAction>
  367. </NoteActions>
  368. </LinkContainer>
  369. ))
  370. }
  371. <LinkContainer>
  372. <Link
  373. href={{
  374. pathname: '/bin',
  375. }}
  376. passHref
  377. >
  378. <NoteLink>
  379. <NavbarItemContent>
  380. <NoteLinkPrimary>
  381. <BinIcon />
  382. <NoteLinkTitle>
  383. View Binned Notes
  384. </NoteLinkTitle>
  385. </NoteLinkPrimary>
  386. </NavbarItemContent>
  387. </NoteLink>
  388. </Link>
  389. </LinkContainer>
  390. </SecondaryNavItemsOverflow>
  391. </SecondaryNavItems>
  392. </NavbarItems>
  393. </NavbarContainer>
  394. </Navbar>
  395. <Main>
  396. <Container>
  397. {
  398. Array.isArray(notes!)
  399. && (
  400. <React.Fragment>
  401. <PostPrimary>
  402. <TitleInput
  403. placeholder="Title"
  404. value={title}
  405. onChange={Note.updateTitle({
  406. stateRef,
  407. timeoutRef,
  408. router,
  409. id,
  410. setNotes,
  411. setTitle,
  412. })}
  413. />
  414. <PostMeta>
  415. {
  416. stateRef.current.updatedAt
  417. && router.query.id
  418. && (
  419. <time
  420. dateTime={new Date(stateRef.current.updatedAt).toISOString()}
  421. >
  422. Last updated {formatDate(new Date(stateRef.current.updatedAt))}
  423. </time>
  424. )
  425. }
  426. </PostMeta>
  427. </PostPrimary>
  428. <Editor
  429. autoFocus={false}
  430. key={id}
  431. content={stateRef.current ? stateRef.current.content : undefined}
  432. onChange={Note.updateContent({
  433. stateRef,
  434. timeoutRef,
  435. router,
  436. id,
  437. setNotes,
  438. })}
  439. placeholder="Start typing here."
  440. />
  441. </React.Fragment>
  442. )
  443. }
  444. </Container>
  445. </Main>
  446. </React.Fragment>
  447. )
  448. }
  449. export const getServerSideProps = async ctx => {
  450. if (ctx.params) {
  451. return {
  452. props: {
  453. id: ctx.params?.id
  454. }
  455. }
  456. }
  457. return {
  458. props: {}
  459. }
  460. }
  461. export default Notes