├── .datignore ├── .gitignore ├── bundles ├── .gitinclude └── CothamSans.otf ├── content.css ├── dropout.json ├── index.html ├── package.json ├── readme.md └── src ├── components ├── button-fork.js ├── button-save.js ├── nav-page.js ├── panel-save.js └── wrapper-site.js ├── design.js ├── index.js ├── plugins ├── dropout.js ├── scroll.js └── ui.js ├── utils └── text.js └── views ├── main.js └── page.js /.datignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | 4 | # dat 5 | .well-known/ 6 | dat.json 7 | 8 | # app 9 | bundles/*.css 10 | bundles/*.js 11 | content/ 12 | !content/.gitinclude -------------------------------------------------------------------------------- /bundles/.gitinclude: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jondashkyle/dropout-app/70394127602ffc5b50820c114935064dff86f9d7/bundles/.gitinclude -------------------------------------------------------------------------------- /bundles/CothamSans.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jondashkyle/dropout-app/70394127602ffc5b50820c114935064dff86f9d7/bundles/CothamSans.otf -------------------------------------------------------------------------------- /content.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: octicons-link; 3 | src: url(data:font/woff;charset=utf-8;base64,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) format('woff'); 4 | } 5 | 6 | .markdown-body img { display: none } /* TEMPORARY */ 7 | 8 | .markdown-body { 9 | -ms-text-size-adjust: 100%; 10 | -webkit-text-size-adjust: 100%; 11 | color: #000; 12 | word-wrap: break-word; 13 | max-width: 50rem; 14 | margin: 0 auto; 15 | } 16 | 17 | .markdown-body .pl-c { 18 | color: #6a737d; 19 | } 20 | 21 | .markdown-body .pl-c1, 22 | .markdown-body .pl-s .pl-v { 23 | color: #005cc5; 24 | } 25 | 26 | .markdown-body .pl-e, 27 | .markdown-body .pl-en { 28 | color: #6f42c1; 29 | } 30 | 31 | .markdown-body .pl-smi, 32 | .markdown-body .pl-s .pl-s1 { 33 | color: #24292e; 34 | } 35 | 36 | .markdown-body .pl-ent { 37 | color: #22863a; 38 | } 39 | 40 | .markdown-body .pl-k { 41 | color: #d73a49; 42 | } 43 | 44 | .markdown-body .pl-s, 45 | .markdown-body .pl-pds, 46 | .markdown-body .pl-s .pl-pse .pl-s1, 47 | .markdown-body .pl-sr, 48 | .markdown-body .pl-sr .pl-cce, 49 | .markdown-body .pl-sr .pl-sre, 50 | .markdown-body .pl-sr .pl-sra { 51 | color: #000; 52 | } 53 | 54 | .markdown-body .pl-v, 55 | .markdown-body .pl-smw { 56 | color: #e36209; 57 | } 58 | 59 | .markdown-body .pl-bu { 60 | color: #b31d28; 61 | } 62 | 63 | .markdown-body .pl-ii { 64 | color: #fafbfc; 65 | background-color: #b31d28; 66 | } 67 | 68 | .markdown-body .pl-c2 { 69 | color: #fafbfc; 70 | background-color: #d73a49; 71 | } 72 | 73 | .markdown-body .pl-c2::before { 74 | content: "^M"; 75 | } 76 | 77 | .markdown-body .pl-sr .pl-cce { 78 | font-weight: bold; 79 | color: #22863a; 80 | } 81 | 82 | .markdown-body .pl-ml { 83 | color: #735c0f; 84 | } 85 | 86 | .markdown-body .pl-mh, 87 | .markdown-body .pl-mh .pl-en, 88 | .markdown-body .pl-ms { 89 | font-weight: bold; 90 | color: #005cc5; 91 | } 92 | 93 | .markdown-body .pl-mi { 94 | font-style: italic; 95 | color: #24292e; 96 | } 97 | 98 | .markdown-body .pl-mb { 99 | font-weight: bold; 100 | color: #24292e; 101 | } 102 | 103 | .markdown-body .pl-md { 104 | color: #b31d28; 105 | background-color: #ffeef0; 106 | } 107 | 108 | .markdown-body .pl-mi1 { 109 | color: #22863a; 110 | background-color: #f0fff4; 111 | } 112 | 113 | .markdown-body .pl-mc { 114 | color: #e36209; 115 | background-color: #ffebda; 116 | } 117 | 118 | .markdown-body .pl-mi2 { 119 | color: #f6f8fa; 120 | background-color: #005cc5; 121 | } 122 | 123 | .markdown-body .pl-mdr { 124 | font-weight: bold; 125 | color: #6f42c1; 126 | } 127 | 128 | .markdown-body .pl-ba { 129 | color: #586069; 130 | } 131 | 132 | .markdown-body .pl-sg { 133 | color: #959da5; 134 | } 135 | 136 | .markdown-body .pl-corl { 137 | text-decoration: underline; 138 | color: #032f62; 139 | } 140 | 141 | .markdown-body .octicon { 142 | display: inline-block; 143 | vertical-align: text-top; 144 | fill: currentColor; 145 | } 146 | 147 | .markdown-body a { 148 | background-color: transparent; 149 | -webkit-text-decoration-skip: objects; 150 | } 151 | 152 | .markdown-body a:active, 153 | .markdown-body a:hover { 154 | outline-width: 0; 155 | } 156 | 157 | .markdown-body strong { 158 | font-weight: inherit; 159 | } 160 | 161 | .markdown-body strong { 162 | font-weight: bolder; 163 | } 164 | 165 | .markdown-body h1 { 166 | font-size: 2em; 167 | margin: 0.67em 0; 168 | } 169 | 170 | .markdown-body img { 171 | border-style: none; 172 | } 173 | 174 | .markdown-body svg:not(:root) { 175 | overflow: hidden; 176 | } 177 | 178 | .markdown-body code, 179 | .markdown-body kbd, 180 | .markdown-body pre { 181 | font-family: monospace, monospace; 182 | font-size: 1em; 183 | } 184 | 185 | .markdown-body hr { 186 | box-sizing: content-box; 187 | height: 0; 188 | overflow: visible; 189 | } 190 | 191 | .markdown-body input { 192 | font: inherit; 193 | margin: 0; 194 | } 195 | 196 | .markdown-body input { 197 | overflow: visible; 198 | } 199 | 200 | .markdown-body [type="checkbox"] { 201 | box-sizing: border-box; 202 | padding: 0; 203 | } 204 | 205 | .markdown-body * { 206 | box-sizing: border-box; 207 | } 208 | 209 | .markdown-body input { 210 | font-family: inherit; 211 | font-size: inherit; 212 | line-height: inherit; 213 | } 214 | 215 | .markdown-body a { 216 | color: #000; 217 | } 218 | 219 | .markdown-body a:hover { 220 | text-decoration: underline; 221 | } 222 | 223 | .markdown-body strong { 224 | font-weight: 600; 225 | } 226 | 227 | .markdown-body hr { 228 | height: 0; 229 | margin: 15px 0; 230 | overflow: hidden; 231 | background: transparent; 232 | border: 0; 233 | border-bottom: 1px solid #dfe2e5; 234 | } 235 | 236 | .markdown-body hr::before { 237 | display: table; 238 | content: ""; 239 | } 240 | 241 | .markdown-body hr::after { 242 | display: table; 243 | clear: both; 244 | content: ""; 245 | } 246 | 247 | .markdown-body table { 248 | border-spacing: 0; 249 | border-collapse: collapse; 250 | } 251 | 252 | .markdown-body td, 253 | .markdown-body th { 254 | padding: 0; 255 | } 256 | 257 | .markdown-body h1, 258 | .markdown-body h2, 259 | .markdown-body h3, 260 | .markdown-body h4, 261 | .markdown-body h5, 262 | .markdown-body h6 { 263 | margin-top: 0; 264 | margin-bottom: 0; 265 | } 266 | 267 | .markdown-body h1 { 268 | font-size: 3em; 269 | font-weight: 600; 270 | } 271 | 272 | .markdown-body h2 { 273 | font-size: 2em; 274 | font-weight: 600; 275 | } 276 | 277 | .markdown-body h3 { 278 | font-size: 1.5em; 279 | font-weight: 600; 280 | } 281 | 282 | .markdown-body h4 { 283 | font-size: 1em; 284 | font-weight: 600; 285 | } 286 | 287 | .markdown-body h5 { 288 | font-size: 1em; 289 | font-weight: 600; 290 | } 291 | 292 | .markdown-body h6 { 293 | font-size: 1em; 294 | font-weight: 600; 295 | } 296 | 297 | .markdown-body p { 298 | margin-top: 0; 299 | margin-bottom: 1.5em; 300 | } 301 | 302 | .markdown-body blockquote { 303 | margin: 0; 304 | } 305 | 306 | .markdown-body ul, 307 | .markdown-body ol { 308 | padding-left: 0; 309 | margin-top: 0; 310 | margin-bottom: 0; 311 | } 312 | 313 | .markdown-body ol ol, 314 | .markdown-body ul ol { 315 | list-style-type: lower-roman; 316 | } 317 | 318 | .markdown-body ul ul ol, 319 | .markdown-body ul ol ol, 320 | .markdown-body ol ul ol, 321 | .markdown-body ol ol ol { 322 | list-style-type: lower-alpha; 323 | } 324 | 325 | .markdown-body dd { 326 | margin-left: 0; 327 | } 328 | 329 | .markdown-body code { 330 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 331 | font-size: 12px; 332 | } 333 | 334 | .markdown-body pre { 335 | margin-top: 0; 336 | margin-bottom: 0; 337 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 338 | font-size: 1em; 339 | } 340 | 341 | .markdown-body .octicon { 342 | vertical-align: text-bottom; 343 | } 344 | 345 | .markdown-body .pl-0 { 346 | padding-left: 0 !important; 347 | } 348 | 349 | .markdown-body .pl-1 { 350 | padding-left: 4px !important; 351 | } 352 | 353 | .markdown-body .pl-2 { 354 | padding-left: 8px !important; 355 | } 356 | 357 | .markdown-body .pl-3 { 358 | padding-left: 16px !important; 359 | } 360 | 361 | .markdown-body .pl-4 { 362 | padding-left: 24px !important; 363 | } 364 | 365 | .markdown-body .pl-5 { 366 | padding-left: 32px !important; 367 | } 368 | 369 | .markdown-body .pl-6 { 370 | padding-left: 40px !important; 371 | } 372 | 373 | .markdown-body::before { 374 | display: table; 375 | content: ""; 376 | } 377 | 378 | .markdown-body::after { 379 | display: table; 380 | clear: both; 381 | content: ""; 382 | } 383 | 384 | .markdown-body>*:first-child { 385 | margin-top: 0 !important; 386 | } 387 | 388 | .markdown-body>*:last-child { 389 | margin-bottom: 0 !important; 390 | } 391 | 392 | .markdown-body a:not([href]) { 393 | color: inherit; 394 | text-decoration: none; 395 | } 396 | 397 | .markdown-body .anchor { 398 | float: left; 399 | padding-right: 4px; 400 | margin-left: -20px; 401 | line-height: 1; 402 | } 403 | 404 | .markdown-body .anchor:focus { 405 | outline: none; 406 | } 407 | 408 | .markdown-body p, 409 | .markdown-body blockquote, 410 | .markdown-body ul, 411 | .markdown-body ol, 412 | .markdown-body dl, 413 | .markdown-body table, 414 | .markdown-body pre { 415 | margin-top: 0; 416 | margin-bottom: 1.5em; 417 | } 418 | 419 | .markdown-body hr { 420 | height: 0.25em; 421 | padding: 0; 422 | margin: 24px 0; 423 | background-color: #e1e4e8; 424 | border: 0; 425 | } 426 | 427 | .markdown-body blockquote { 428 | padding: 0 1em; 429 | color: #6a737d; 430 | border-left: 0.25em solid #dfe2e5; 431 | } 432 | 433 | .markdown-body blockquote>:first-child { 434 | margin-top: 0; 435 | } 436 | 437 | .markdown-body blockquote>:last-child { 438 | margin-bottom: 0; 439 | } 440 | 441 | .markdown-body kbd { 442 | display: inline-block; 443 | padding: 3px 5px; 444 | font-size: 11px; 445 | line-height: 10px; 446 | color: #444d56; 447 | vertical-align: middle; 448 | background-color: #fafbfc; 449 | border: solid 1px #c6cbd1; 450 | border-bottom-color: #959da5; 451 | border-radius: 3px; 452 | box-shadow: inset 0 -1px 0 #959da5; 453 | } 454 | 455 | .markdown-body h1, 456 | .markdown-body h2, 457 | .markdown-body h3, 458 | .markdown-body h4, 459 | .markdown-body h5, 460 | .markdown-body h6 { 461 | margin-top: 1.5em; 462 | margin-bottom: 0.75em; 463 | font-weight: 600; 464 | } 465 | 466 | .markdown-body h1 .octicon-link, 467 | .markdown-body h2 .octicon-link, 468 | .markdown-body h3 .octicon-link, 469 | .markdown-body h4 .octicon-link, 470 | .markdown-body h5 .octicon-link, 471 | .markdown-body h6 .octicon-link { 472 | color: #1b1f23; 473 | vertical-align: middle; 474 | visibility: hidden; 475 | } 476 | 477 | .markdown-body h1:hover .anchor, 478 | .markdown-body h2:hover .anchor, 479 | .markdown-body h3:hover .anchor, 480 | .markdown-body h4:hover .anchor, 481 | .markdown-body h5:hover .anchor, 482 | .markdown-body h6:hover .anchor { 483 | text-decoration: none; 484 | } 485 | 486 | .markdown-body h1:hover .anchor .octicon-link, 487 | .markdown-body h2:hover .anchor .octicon-link, 488 | .markdown-body h3:hover .anchor .octicon-link, 489 | .markdown-body h4:hover .anchor .octicon-link, 490 | .markdown-body h5:hover .anchor .octicon-link, 491 | .markdown-body h6:hover .anchor .octicon-link { 492 | visibility: visible; 493 | } 494 | 495 | .markdown-body h1 { 496 | padding-bottom: 0.3em; 497 | font-size: 2em; 498 | border-bottom: 1px solid #eaecef; 499 | } 500 | 501 | .markdown-body h2 { 502 | padding-bottom: 0.3em; 503 | border-bottom: 1px solid #eaecef; 504 | } 505 | 506 | .markdown-body h3 { 507 | } 508 | 509 | .markdown-body h4 { 510 | } 511 | 512 | .markdown-body h5 { 513 | } 514 | 515 | .markdown-body h6 { 516 | color: #6a737d; 517 | } 518 | 519 | .markdown-body ul, 520 | .markdown-body ol { 521 | padding-left: 2em; 522 | } 523 | 524 | .markdown-body ul ul, 525 | .markdown-body ul ol, 526 | .markdown-body ol ol, 527 | .markdown-body ol ul { 528 | margin-top: 0; 529 | margin-bottom: 0; 530 | } 531 | 532 | .markdown-body li>p { 533 | margin-top: 1.5em; 534 | } 535 | 536 | .markdown-body li+li { 537 | margin-top: 0.25em; 538 | } 539 | 540 | .markdown-body dl { 541 | padding: 0; 542 | } 543 | 544 | .markdown-body dl dt { 545 | padding: 0; 546 | margin-top: 16px; 547 | font-size: 1em; 548 | font-style: italic; 549 | font-weight: 600; 550 | } 551 | 552 | .markdown-body dl dd { 553 | padding: 0 16px; 554 | margin-bottom: 16px; 555 | } 556 | 557 | .markdown-body table { 558 | display: block; 559 | width: 100%; 560 | overflow: auto; 561 | } 562 | 563 | .markdown-body table th { 564 | font-weight: 600; 565 | } 566 | 567 | .markdown-body table th, 568 | .markdown-body table td { 569 | padding: 6px 13px; 570 | border: 1px solid #dfe2e5; 571 | } 572 | 573 | .markdown-body table tr { 574 | background-color: #fff; 575 | border-top: 1px solid #c6cbd1; 576 | } 577 | 578 | .markdown-body table tr:nth-child(2n) { 579 | background-color: #f6f8fa; 580 | } 581 | 582 | .markdown-body img { 583 | max-width: 100%; 584 | box-sizing: content-box; 585 | background-color: #fff; 586 | } 587 | 588 | .markdown-body img[align=right] { 589 | padding-left: 20px; 590 | } 591 | 592 | .markdown-body img[align=left] { 593 | padding-right: 20px; 594 | } 595 | 596 | .markdown-body code { 597 | padding: 0; 598 | padding-top: 0.2em; 599 | padding-bottom: 0.2em; 600 | margin: 0; 601 | font-size: 85%; 602 | background-color: rgba(27,31,35,0.05); 603 | border-radius: 3px; 604 | } 605 | 606 | .markdown-body code::before, 607 | .markdown-body code::after { 608 | letter-spacing: -0.2em; 609 | content: "\00a0"; 610 | } 611 | 612 | .markdown-body pre { 613 | word-wrap: normal; 614 | } 615 | 616 | .markdown-body pre>code { 617 | padding: 0; 618 | margin: 0; 619 | font-size: 100%; 620 | word-break: normal; 621 | white-space: pre; 622 | background: transparent; 623 | border: 0; 624 | } 625 | 626 | .markdown-body .highlight { 627 | margin-bottom: 16px; 628 | } 629 | 630 | .markdown-body .highlight pre { 631 | margin-bottom: 0; 632 | word-break: normal; 633 | } 634 | 635 | .markdown-body .highlight pre, 636 | .markdown-body pre { 637 | padding: 16px; 638 | overflow: auto; 639 | font-size: 85%; 640 | line-height: 1.45; 641 | background-color: #f6f8fa; 642 | border-radius: 3px; 643 | } 644 | 645 | .markdown-body pre code { 646 | display: inline; 647 | max-width: auto; 648 | padding: 0; 649 | margin: 0; 650 | overflow: visible; 651 | line-height: inherit; 652 | word-wrap: normal; 653 | background-color: transparent; 654 | border: 0; 655 | } 656 | 657 | .markdown-body pre code::before, 658 | .markdown-body pre code::after { 659 | content: normal; 660 | } 661 | 662 | .markdown-body .full-commit .btn-outline:not(:disabled):hover { 663 | color: #005cc5; 664 | border-color: #005cc5; 665 | } 666 | 667 | .markdown-body kbd { 668 | display: inline-block; 669 | padding: 3px 5px; 670 | font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; 671 | line-height: 10px; 672 | color: #444d56; 673 | vertical-align: middle; 674 | background-color: #fafbfc; 675 | border: solid 1px #d1d5da; 676 | border-bottom-color: #c6cbd1; 677 | border-radius: 3px; 678 | box-shadow: inset 0 -1px 0 #c6cbd1; 679 | } 680 | 681 | .markdown-body :checked+.radio-label { 682 | position: relative; 683 | z-index: 1; 684 | border-color: #0366d6; 685 | } 686 | 687 | .markdown-body .task-list-item { 688 | list-style-type: none; 689 | } 690 | 691 | .markdown-body .task-list-item+.task-list-item { 692 | margin-top: 3px; 693 | } 694 | 695 | .markdown-body .task-list-item input { 696 | margin: 0 0.2em 0.25em -1.6em; 697 | vertical-align: middle; 698 | } 699 | 700 | .markdown-body hr { 701 | border-bottom-color: #eee; 702 | } -------------------------------------------------------------------------------- /dropout.json: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "/content", 3 | "microservice": "https://melodic-comfort.glitch.me/", 4 | "dateformat": "mmmm dS", 5 | "forkable": true 6 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dropout 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dropout-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "deploy": "dat sync --no-ignoreHidden", 8 | "start": "watchify -o bundles/bundle.js -t browserify-nodent -t sheetify -p [ css-extract -o bundles/bundle.css ] src/index.js", 9 | "build": "browserify -t browserify-nodent -t [ sheetify -u sheetify-cssnext ] -p [ css-extract -o bundles/bundle.css ] -p common-shakeify -p browser-pack-flat/plugin src/index.js -g uglifyify > bundles/bundle.js" 10 | }, 11 | "keywords": [], 12 | "author": "Jon-Kyle (http://jon-kyle.com)", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "browser-pack-flat": "^3.0.3", 16 | "browserify": "^14.4.0", 17 | "browserify-nodent": "^1.0.22", 18 | "common-shakeify": "^0.4.4", 19 | "css-extract": "^1.2.0", 20 | "sheetify-cssnext": "^1.0.7", 21 | "uglifyify": "^4.0.4", 22 | "watchify": "^3.9.0" 23 | }, 24 | "dependencies": { 25 | "bel": "^5.1.3", 26 | "choo": "^6.5.1", 27 | "dateformat": "^3.0.2", 28 | "dropout": "^1.0.0", 29 | "gr8": "^3.1.3", 30 | "nanocomponent": "^6.4.2", 31 | "object-keys": "^1.0.11", 32 | "object-values": "^1.0.0", 33 | "raf": "^3.4.0", 34 | "recsst": "^1.1.2", 35 | "sheetify": "^6.2.0", 36 | "url-join": "^2.0.2", 37 | "xhr": "^2.4.0", 38 | "xtend": "^4.0.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

dropout-app

2 | 3 | Dropout of the centralized web and into the p2p web! This is a webapp for using [`dropout`](https://github.com/jondashkyle/dropout) with [Beaker Browser](https://beakerbrowser.com). It’s sort of like your own fully customizable and offline version of Instapaper. In addition to being a fully usable webapp, this might also be a nice peek into Choo architecture and Beaker’s web API. 4 | 5 | ## usage 6 | 7 | First [download and install Beaker Browser](https://beakerbrowser.com/docs/install/), then visit the Dat URL. Once there, click the Fork button in the lower right! 8 | 9 | ``` 10 | dat://dropout.jon-kyle.com 11 | dat://8b79c46e3484ae0f1fbe530711a762214543f2c37c4d323cb523450927b6f042 12 | ``` 13 | 14 | Dropout uses Beaker’s experimental [web api to read and write](https://beakerbrowser.com/docs/apis/) to the Dat archive, which requires you to be connected through `dat://` and not `https://`. If the web api is not available a message is displayed detailing the necessary steps to get going. 15 | 16 | [![](http://drop.jon-kyle.com/modules/dropout-beaker-2.png)](http://dropout.jon-kyle.com) 17 | 18 | ## structure and functionality 19 | 20 | ### scraping and saving 21 | 22 | [Dropout](https://github.com/jondashkyle/dropout) (the module) scrapes a URL and retrieves the document and cleans it up, providing you with only the content. This process requires the [`node-readability`](https://github.com/luin/readability) module. 23 | 24 | Sites deployed with Dat and viewable in Beaker are static, meaning they can not execute modules which utilize binaries. The readability module must be run on a server. To get around this limitation a request to an easily [self-deployable microservice](https://github.com/jondashkyle/dropout-service) is made from within the webapp. Of course, this requires an internet connection, however the limitation would be present even if able to run strictly within the client as a request to the page being scraped must be made. 25 | 26 | To run you very own instance, simply navigate to [Glitch and click the Fork button](https://glitch.com/edit/#!/melodic-comfort?path=index.js:1:0). Glitch will generate a URL for you. Copy that and open `dropout.json` in the root of your app. Paste it as the value of `microservice`. That’s it! 27 | 28 | You might be wondering, “If you can’t run server side code, how do you save data without using localstorage?” What’s awesome about Dat and Beaker is “Everything Is A File.” Beaker’s web api mirrors that of Node’s `fs` module. Saving data is as simple as reading and writing folders and files. 29 | 30 | ## customization 31 | 32 | ### design and functionality 33 | 34 | The webapp is built with [`choo`](https://github.com/choojs/choo), a super lightweight and understandable front-end framework. Open your site directory in terminal and run `npm install`. The site’s source is contained in `src/`, and can be hacked away however you’d like. 35 | 36 | You can also adjust the design of the page content without being required to install and run any build process. Just edit `content.css` and you’re golden! 37 | 38 | ### privacy 39 | 40 | With Beaker, it’s possible to see the source files of your Dropout library. It’s also possible to fork/clone your full site. These are great features, but sometimes it’s nice to keep things private. To do so, just add the `content` directory to `.datignore`. 41 | 42 | ## known bugs 43 | 44 | Want to contribute? Please do! Alternatively, donations and funding are being accepted for dedicating time to design and development of features you’d like to see. Get in touch if you’re curious. 45 | 46 | ### image assets do not load 47 | 48 | Beaker requires all connections to be over https, preventing external assets such as images from being loaded over http. Of course this is desired functionality, and ultimately media assets (images, videos, audio) should be detected and scraped, too. This is a somewhat more extensive feature which will hopefully be added soon. 49 | 50 | ## elsewhere 51 | 52 | - Set in [Cotham](https://github.com/sebsan/Cotham) by Sebastien Sanfilippo of [Love Letters](http://www.love-letters.be/) 53 | 54 | ## todo 55 | 56 | - [ ] General design 57 | - [ ] Fallback to localstorage if the web api is not available 58 | - [ ] Export from localstorage to JSON, drag into app 59 | - [ ] Save static HTML in addition to JSON 60 | - [ ] Scrape image assets (breaks with http) 61 | - [ ] Search/filter page list 62 | - [ ] Settings Panel (design customization) 63 | - [ ] Separate the App from the User data (Fritter model) 64 | - [ ] Mark as read when reaching bottom of page 65 | - [ ] Keyboard shortcuts 66 | - [ ] Swap out arrow icons for non sf-mono svgs 67 | 68 | ## change log 69 | 70 | ### 10/28/2017 71 | 72 | First release! Apart from making the damn thing there were these other items: 73 | 74 | - [x] Sticky navigation 75 | - [x] Deploy on server and add dat 76 | - [x] Show fork instead of add if not Dat Owner 77 | - [X] Cloning/Forking UI 78 | -------------------------------------------------------------------------------- /src/components/button-fork.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var assert = require('assert') 3 | 4 | module.exports = buttonFork 5 | 6 | function buttonFork (props) { 7 | assert.equal(typeof props, 'object', 'arg1 props should be type object') 8 | assert.equal(typeof props.onclick, 'function', 'arg1.onclick props should be type function') 9 | 10 | return html` 11 |
12 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | ` 26 | } -------------------------------------------------------------------------------- /src/components/button-save.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var assert = require('assert') 3 | var css = require('sheetify') 4 | 5 | var style = css` 6 | .button-animate { 7 | transition: transform 250ms cubic-bezier(0.215, 0.61, 0.355, 1); 8 | transform: rotate(0deg); 9 | } 10 | 11 | .button-animate.button-rotate { 12 | transform: rotate(45deg); 13 | } 14 | ` 15 | 16 | module.exports = buttonSave 17 | 18 | function buttonSave (props) { 19 | assert.equal(typeof props, 'object', 'arg1 props should be type object') 20 | assert.equal(typeof props.onclick, 'function', 'arg1.onclick props should be type function') 21 | 22 | return html` 23 |
24 |
28 |
+
29 |
30 |
31 | ` 32 | } -------------------------------------------------------------------------------- /src/components/nav-page.js: -------------------------------------------------------------------------------- 1 | var Nanocomponent = require('nanocomponent') 2 | var html = require('choo/html') 3 | var assert = require('assert') 4 | var css = require('sheetify') 5 | var raw = require('bel/raw') 6 | var raf = require('raf') 7 | 8 | var style = css` 9 | :host { 10 | transform: translate3d(0, 0, 0); 11 | transition: transform 250ms cubic-bezier(0.215, 0.61, 0.355, 1); 12 | } 13 | 14 | :host.nav-page-hide { 15 | transform: translate3d(0, -100%, 0); 16 | } 17 | ` 18 | 19 | module.exports = class NavPage extends Nanocomponent { 20 | constructor () { 21 | super() 22 | 23 | this.state = { 24 | scrollY: 0, 25 | active: false, 26 | basename: '' 27 | } 28 | 29 | this.frame 30 | this.handleScroll = this.handleScroll.bind(this) 31 | } 32 | 33 | load () { 34 | var self = this 35 | setTimeout(function () { 36 | self.frame = raf(self.handleScroll) 37 | self.state.active = true 38 | self.rerender() 39 | }, 100) 40 | } 41 | 42 | unload () { 43 | raf.cancel(this.frame) 44 | this.state.active = false 45 | this.state.scrollY = 0 46 | } 47 | 48 | handleScroll () { 49 | var scrollY = window.scrollY 50 | if (scrollY === this.state.scrollY) { 51 | this.frame = raf(this.handleScroll) 52 | return 53 | } else { 54 | if (scrollY > this.state.scrollY && scrollY > 100) { 55 | this.hide() 56 | } else { 57 | this.show() 58 | } 59 | this.state.scrollY = scrollY 60 | this.frame = raf(this.handleScroll) 61 | } 62 | } 63 | 64 | show () { 65 | if (!this.state.active) { 66 | this.state.active = true 67 | this.rerender() 68 | } 69 | } 70 | 71 | hide () { 72 | if (this.state.active) { 73 | this.state.active = false 74 | this.rerender() 75 | } 76 | } 77 | 78 | createElement (props) { 79 | assert(typeof props, 'object', 'arg1 props must be type object') 80 | assert(typeof props.page, 'object', 'arg1 props.page must be type object') 81 | assert(typeof props.handleDelete, 'fucntion', 'arg1 props.handleDelete must be type function') 82 | 83 | return html` 84 |
85 |
86 | Library 87 |
88 |
${props.page.title}
89 |
90 | ${props.page.source ? source() : ''} 91 |
Delete
92 |
93 |
94 | ` 95 | 96 | function source () { 97 | return html` 98 | 102 | Source 103 | 104 | ` 105 | } 106 | } 107 | 108 | update (props) { 109 | console.log(props) 110 | } 111 | } -------------------------------------------------------------------------------- /src/components/panel-save.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var html = require('choo/html') 3 | 4 | module.exports = panelSave 5 | 6 | function panelSave (props) { 7 | assert.equal(typeof props, 'object', 'arg1 props must be type object') 8 | assert.equal(typeof props.value, 'string', 'arg1 props.value must be type string') 9 | assert.equal(typeof props.oninput, 'function', 'arg1 props.oninput must be type function') 10 | assert.equal(typeof props.onsubmit, 'function', 'arg1 props.onsubmit must be type function') 11 | 12 | var placeholder = props.placeholder || 'http://' 13 | 14 | return html` 15 |
16 | 25 |
26 |
${props.saving ? 'Saving...' : 'Save to Library'}
30 |
31 | 32 | ` 33 | 34 | function handleInput (event) { 35 | props.oninput(event.target.value) 36 | } 37 | 38 | function handleSubmit (event) { 39 | props.onsubmit() 40 | event.preventDefault() 41 | } 42 | } -------------------------------------------------------------------------------- /src/components/wrapper-site.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | 3 | module.exports = wrapper 4 | 5 | function wrapper (view) { 6 | return function (state, emit) { 7 | if (!state.dropout) return noArchive() 8 | 9 | return html` 10 | 11 | ${view(state, emit)} 12 | 13 | ` 14 | } 15 | } 16 | 17 | function noArchive (state, emit) { 18 | return html` 19 | 20 |
21 |

When interfaces are designed for capturing and exhausting your attention going offline is both an act of liberation and luxury. This is a tool of ethical technology enabling you to save pages for offline access and personal archival.

22 |

The project requires Beaker Browser and it's experimental web api to function. Good news; it's really easy to start, and once downloaded you can begin browsing and publishing to the rest of the p2p web, too.

23 |
    24 |
  1. Download and install Beaker Browser.
  2. 25 |
  3. Navigate to the Dat archive url.
  4. 26 |
  5. If you'd like to create your own, click Fork inside Beaker. Feel free to customize as the entire source is included.
  6. 27 |
28 |

For more, visit the repository or the log entry. 29 |

30 | 31 | ` 32 | } 33 | -------------------------------------------------------------------------------- /src/design.js: -------------------------------------------------------------------------------- 1 | var gr8 = require('gr8') 2 | 3 | var typography = { 4 | sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif', 5 | heading: '"Cotham", sans-serif', 6 | mono: '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace' 7 | } 8 | 9 | var colors = { 10 | white: '#fff', 11 | greyish: 'rgba(230, 230, 230, 0.95)', 12 | black: '#000', 13 | transparent: 'transparent' 14 | } 15 | 16 | var utils = [ ] 17 | 18 | utils.push({ 19 | prop: 'font-family', 20 | join: '-', 21 | vals: typography 22 | }) 23 | 24 | utils.push({ 25 | prop: { bgc: 'background-color' }, 26 | join: '-', 27 | vals: colors 28 | }) 29 | 30 | utils.push({ 31 | prop: { fc: 'color' }, 32 | join: '-', 33 | vals: colors 34 | }) 35 | 36 | utils.push({ 37 | prop: { oph: 'opacity' }, 38 | tail: ':hover', 39 | vals: [0, 25, 50, 75, 100] 40 | }) 41 | 42 | utils.push({ 43 | prop: { pvw: 'padding' }, 44 | unit: 'vw', 45 | vals: [0, 0.5, 1, 1.5, 2] 46 | }) 47 | 48 | utils.push({ 49 | prop: { fsvw: 'font-size' }, 50 | unit: 'vw', 51 | vals: [1, 1.5, 2, 2.5, 5, 6] 52 | }) 53 | 54 | utils.push({ 55 | prop: 'font-weight', 56 | vals: ['normal', 'bold', 200, 300, 500, 800] 57 | }) 58 | 59 | var borderWeights = [1] 60 | var borders = {} 61 | borderWeights.forEach(border => { 62 | Object.keys(colors).forEach(key => { 63 | borders[border + '-' + key] = `${border}px solid ${colors[key]}` 64 | }) 65 | }) 66 | 67 | utils.push({ 68 | prop: [ 69 | 'border', 70 | 'border-top', 71 | 'border-right', 72 | 'border-bottom', 73 | 'border-left' 74 | ], 75 | vals: borders 76 | }) 77 | 78 | var gr8css = gr8({ 79 | lineHeight: [1, 1.1, 1.25, 1.5], 80 | spacing: [0.5, 1, 1.5, 2, 3, 4], 81 | fontSize: [0, 0.75, 1, 1.5, 2, 3], 82 | utils: utils 83 | }) 84 | 85 | var custom = ` 86 | html { 87 | font-size: 100%; 88 | -webkit-font-smoothing: antialiased; 89 | -moz-osx-font-smoothing: grayscale; 90 | -moz-font-feature-settings:"kern" 1; 91 | -ms-font-feature-settings:"kern" 1; 92 | -o-font-feature-settings:"kern" 1; 93 | -webkit-font-feature-settings:"kern" 1; 94 | font-feature-settings:"kern" 1; 95 | font-kerning: normal 96 | } 97 | 98 | .fab { 99 | border-radius: 1.5rem; 100 | text-align: center; 101 | line-height: 3rem; 102 | font-weight: 300; 103 | font-size: 2rem; 104 | height: 3rem; 105 | width: 3rem; 106 | } 107 | 108 | .button-wide { 109 | border-radius: 2rem; 110 | height: 3rem; 111 | line-height: 3rem; 112 | } 113 | 114 | .ti1 { 115 | padding-left: 1em; 116 | text-indent: -1em; 117 | } 118 | 119 | .toe { 120 | text-overflow: ellipsis; 121 | white-space: nowrap; 122 | overflow: hidden; 123 | } 124 | 125 | .panel-save { 126 | opacity: 0; 127 | transition: opacity 300ms cubic-bezier(0.215, 0.61, 0.355, 1); 128 | } 129 | 130 | .panel-save-active { 131 | opacity: 1; 132 | } 133 | 134 | .panel-save input { 135 | transform: translate3d(0, 1rem, 0); 136 | transition: transform 300ms cubic-bezier(0.215, 0.61, 0.355, 1); 137 | } 138 | 139 | .panel-save-active input { 140 | transform: translate3d(0, 0, 0); 141 | } 142 | 143 | .button-wide { 144 | opacity: 0; 145 | transform: translate3d(0, -1rem, 0); 146 | transition: transform 300ms cubic-bezier(0.215, 0.61, 0.355, 1), opacity 300ms cubic-bezier(0.215, 0.61, 0.355, 1); 147 | } 148 | 149 | .button-wide-active { 150 | opacity: 1; 151 | transform: translate3d(0, 0, 0); 152 | } 153 | 154 | .curt { cursor: text } 155 | input { -webkit-appearance: none } 156 | 157 | ::-webkit-input-placeholder { color: ${colors.black}; } 158 | ::-moz-placeholder { color: ${colors.black}; } 159 | :-ms-input-placeholder { color: ${colors.black}; } 160 | :-moz-placeholder { color: ${colors.black}; } 161 | 162 | @font-face { 163 | font-family: 'Cotham'; 164 | src: url('/bundles/CothamSans.otf'); 165 | } 166 | ` 167 | 168 | module.exports = gr8css + custom 169 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var css = require('sheetify') 3 | var choo = require('choo') 4 | 5 | var dropout = require('./plugins/dropout') 6 | var scroll = require('./plugins/scroll') 7 | var ui = require('./plugins/ui') 8 | 9 | css('recsst/recsst.css') 10 | css('./design.js') 11 | 12 | var app = choo() 13 | 14 | app.use(dropout()) 15 | app.use(scroll()) 16 | app.use(ui()) 17 | 18 | app.route('/', require('./views/main')) 19 | app.route('/:page', require('./views/page')) 20 | 21 | if (module.parent) module.exports = app 22 | else app.mount('body') -------------------------------------------------------------------------------- /src/plugins/dropout.js: -------------------------------------------------------------------------------- 1 | var urlJoin = require('url-join') 2 | var assert = require('assert') 3 | var xtend = require('xtend') 4 | var path = require('path') 5 | var xhr = require('xhr') 6 | 7 | module.exports = expose 8 | 9 | function expose (datUrl) { 10 | return function plugin (state, emitter, app) { 11 | try { 12 | var archive = new DatArchive(datUrl || window.location.toString()) 13 | } catch (err) { 14 | return noArchive() 15 | } 16 | 17 | var fs = makeDatFs(archive) 18 | 19 | state.events = state.events || { } 20 | state.dropout = { 21 | loaded: false, 22 | saving: false, 23 | error: '', 24 | library: { }, 25 | config: { 26 | microservice: '', 27 | dateformat: 'mmmm dS' 28 | }, 29 | dat: { } 30 | } 31 | 32 | state.events.DROPOUT_WRITEPAGE = 'dropout-writepage' 33 | state.events.DROPOUT_DELETEPAGE = 'dropout-deletepage' 34 | state.events.DROPOUT_SAVEPAGE = 'dropout-savepage' 35 | state.events.DROPOUT_FORK = 'dropout-fork' 36 | 37 | emitter.on(state.events.DOMCONTENTLOADED, loaded) 38 | emitter.on(state.events.DROPOUT_WRITEPAGE, writePage) 39 | emitter.on(state.events.DROPOUT_DELETEPAGE, deletePage) 40 | emitter.on(state.events.DROPOUT_SAVEPAGE, savePage) 41 | emitter.on(state.events.DROPOUT_FORK, fork) 42 | 43 | async function loaded () { 44 | var config = await archive.readFile('/dropout.json') 45 | state.dropout.config = xtend(state.dropout.config, JSON.parse(config)) 46 | state.dropout.dat = await archive.getInfo() 47 | await refresh() 48 | state.dropout.loaded = true 49 | } 50 | 51 | async function refresh () { 52 | // very messy 53 | try { 54 | var input 55 | input = await fs.readdir(state.dropout.config.directory) 56 | input = input.filter(file => !/(^[.#]|(?:__|~)$)/.test(file)) 57 | input.forEach(async function (file, i) { 58 | var content = await readPage(file) 59 | state.dropout.library[file] = content 60 | if (i === input.length - 1) emitter.emit(state.events.RENDER) 61 | }) 62 | if (!input.length) emitter.emit(state.events.RENDER) 63 | } catch (err) { 64 | await fs.mkdir(state.dropout.config.directory) 65 | emitter.emit(state.events.RENDER) 66 | } 67 | } 68 | 69 | async function readPage (basename) { 70 | var dirParent = path.join(state.dropout.config.directory, basename) 71 | var content = await archive.readFile(path.join(dirParent, 'index.json')) 72 | return JSON.parse(content) 73 | } 74 | 75 | async function writePage (data) { 76 | assert.equal(typeof data, 'object', 'DROPOUT_WRITEPAGE arg1 data must be type object') 77 | assert.equal(typeof data.basename, 'string', 'DROPOUT_WRITEPAGE arg1 data.basename must be type string') 78 | 79 | var dirParent = path.join(state.dropout.config.directory, data.basename) 80 | var dataPage = state.dropout.library[data.basename] 81 | var pageExists = !!dataPage 82 | var dataOutput = xtend(dataPage, data) 83 | 84 | if (!pageExists) await fs.mkdir(dirParent) 85 | await fs.writeFile(path.join(dirParent, 'index.json'), JSON.stringify(dataOutput, { }, 2)) 86 | state.dropout.saving = false 87 | state.dropout.library[data.basename] = dataOutput 88 | } 89 | 90 | async function deletePage (data) { 91 | assert.equal(typeof data, 'object', 'DROPOUT_DELETEPAGE arg1 data must be type object') 92 | assert.equal(typeof data.basename, 'string', 'DROPOUT_DELETEPAGE arg1 data.basename must be type string') 93 | 94 | var dirParent = path.join(state.dropout.config.directory, data.basename) 95 | 96 | await fs.rmdir(dirParent, { recursive: true }) 97 | delete state.dropout.library[data.basename] 98 | if (data.redirect) emitter.emit(state.events.PUSHSTATE, data.redirect) 99 | } 100 | 101 | async function savePage (data) { 102 | assert.equal(typeof data, 'object', 'DROPOUT_SAVEPAGE arg1 data must be type object') 103 | assert.equal(typeof data.url, 'string', 'DROPOUT_SAVEPAGE arg1 data.url must be type string') 104 | 105 | state.dropout.saving = true 106 | emitter.emit(state.events.RENDER) 107 | 108 | xhr.post({ 109 | url: state.dropout.config.microservice, 110 | body: 'url=' + data.url, 111 | headers: { 112 | 'Content-Type': 'application/x-www-form-urlencoded' 113 | } 114 | }, function (err, resp, body) { 115 | state.dropout.saving = false 116 | if (err) { 117 | emitter.emit(state.events.RENDER) 118 | throw err 119 | } 120 | var response = JSON.parse(body, { }, 2) 121 | await writePage(response) 122 | if (typeof data.callback === 'function') data.callback(response) 123 | if (data.render !== false) emitter.emit(state.events.RENDER) 124 | }) 125 | } 126 | 127 | function noArchive () { 128 | state.dropout = false 129 | emitter.emit(state.events.RENDER) 130 | } 131 | 132 | async function fork (data) { 133 | data = data || { } 134 | await archive.download() 135 | var forkedArchive = await DatArchive.fork(state.dropout.dat.url) 136 | var forkedConfig = await forkedArchive.readFile('/dropout.json') 137 | 138 | forkedConfig.forkable = false 139 | await forkedArchive.writeFile('/dropout.json', forkedConfig) 140 | await forkedArchive.rmdir(state.dropout.config.directory, { recursive: true }) 141 | if (data.redirect !== false) window.location = forkedArchive.url 142 | } 143 | } 144 | } 145 | 146 | function makeDatFs (archive) { 147 | return { 148 | readFile: readFile, 149 | writeFile: writeFile, 150 | readdir: readdir, 151 | mkdir: mkdir, 152 | rmdir: rmdir, 153 | unlink: unlink, 154 | stat: stat 155 | } 156 | 157 | async function readFile (dir, opts) { 158 | return await archive.readFile(dir, opts) 159 | } 160 | 161 | async function writeFile (dir, data) { 162 | return await archive.writeFile(dir, data) 163 | } 164 | 165 | async function readdir (dir, opts) { 166 | return await archive.readdir(dir, opts) 167 | } 168 | 169 | async function mkdir (dir, opts) { 170 | return await archive.mkdir(dir, opts) 171 | } 172 | 173 | async function rmdir (dir, opts) { 174 | return await archive.rmdir(dir, opts) 175 | } 176 | 177 | async function unlink (dir) { 178 | return await archive.unlink(dir) 179 | } 180 | 181 | async function stat (dir) { 182 | return await archive.stat(dir) 183 | } 184 | } -------------------------------------------------------------------------------- /src/plugins/scroll.js: -------------------------------------------------------------------------------- 1 | module.exports = expose 2 | 3 | function expose () { 4 | return function scroll (state, emitter, app) { 5 | emitter.on(state.events.NAVIGATE, navigate) 6 | 7 | function navigate () { 8 | window.scrollTo(0, 0) 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/plugins/ui.js: -------------------------------------------------------------------------------- 1 | var xtend = require('xtend') 2 | 3 | module.exports = expose 4 | 5 | function expose () { 6 | return function plugin (state, emitter) { 7 | state.ui = { 8 | add: { 9 | active: false, 10 | value: '' 11 | } 12 | } 13 | 14 | state.events.UI = 'ui' 15 | state.events.UI_ADD = 'ui-add' 16 | 17 | emitter.on(state.events.UI, ui) 18 | emitter.on(state.events.UI_ADD, add) 19 | 20 | function ui (data) { 21 | 22 | } 23 | 24 | function add (data) { 25 | if (!data) return 26 | state.ui.add = xtend(state.ui.add, data) 27 | if (data.render !== false) emitter.emit(state.events.RENDER) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/text.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jondashkyle/dropout-app/70394127602ffc5b50820c114935064dff86f9d7/src/utils/text.js -------------------------------------------------------------------------------- /src/views/main.js: -------------------------------------------------------------------------------- 1 | var objectKeys = require('object-keys') 2 | var dateFormat = require('dateformat') 3 | var html = require('choo/html') 4 | var raw = require('bel/raw') 5 | 6 | var buttonFork = require('../components/button-fork') 7 | var buttonSave = require('../components/button-save') 8 | var panelSave = require('../components/panel-save') 9 | var wrapper = require('../components/wrapper-site') 10 | 11 | module.exports = wrapper(view) 12 | 13 | function view (state, emit) { 14 | return html` 15 |
16 | ${elPanelSave()} 17 | ${state.dropout.dat.isOwner 18 | ? elSavePage() 19 | : state.dropout.config.forkable ? elFork() : '' 20 | } 21 | ${state.dropout.loaded 22 | ? pages({ pages: getPages() }) 23 | : '' 24 | } 25 |
26 | ` 27 | 28 | function getPages () { 29 | return objectKeys(state.dropout.library) 30 | .map(function (key) { 31 | var output = state.dropout.library[key] 32 | output.basename = output.basename || key 33 | output.dateformat = state.dropout.config.dateformat 34 | return output 35 | }) 36 | .sort(function (a, b) { 37 | return b.date - a.date 38 | }) 39 | } 40 | 41 | function elPanelSave () { 42 | return html` 43 |
47 | ${panelSave({ 48 | saving: state.dropout.saving, 49 | oninput: oninput, 50 | onsubmit: onsubmit, 51 | value: state.ui.add.value 52 | })} 53 |
54 | ` 55 | 56 | function oninput (data) { 57 | emit(state.events.UI_ADD, { value: data }) 58 | } 59 | 60 | function onsubmit () { 61 | emit(state.events.DROPOUT_SAVEPAGE, { 62 | url: state.ui.add.value, 63 | callback: onsuccess, 64 | render: false 65 | }) 66 | } 67 | 68 | function onsuccess () { 69 | emit(state.events.UI_ADD, { active: false, value: '' }) 70 | setTimeout (function () { 71 | emit(state.events.UI_ADD, { value: '' }) 72 | }, 500) 73 | } 74 | 75 | function handleContainerClick (event) { 76 | focusInput(event) 77 | } 78 | } 79 | 80 | function elSavePage() { 81 | return buttonSave({ 82 | active: state.ui.add.active, 83 | onclick: function (event) { 84 | emit(state.events.UI_ADD, { active: !state.ui.add.active }) 85 | emit(state.events.RENDER) 86 | if (!state.ui.add.active) { 87 | setTimeout(function () { 88 | emit(state.events.UI_ADD, { value: '' }) 89 | }, 500) 90 | } else { 91 | setTimeout(focusInput, 0) 92 | } 93 | } 94 | }) 95 | } 96 | 97 | function elFork () { 98 | return buttonFork({ 99 | active: false, 100 | onclick: function () { 101 | emit(state.events.DROPOUT_FORK) 102 | } 103 | }) 104 | } 105 | 106 | function focusInput (event) { 107 | if (!state.ui.add.active) return 108 | var el = document.querySelector('input') 109 | if (el) el.focus() 110 | } 111 | } 112 | 113 | function pages (props) { 114 | if (props.pages.length) { 115 | return html` 116 |
117 | ${props.pages.map(page)} 118 |
119 | ` 120 | } else { 121 | return html` 122 |
123 |
124 | Your library is empty! Click the add button in the lower-right to get started. 125 |
126 |
127 | ` 128 | } 129 | } 130 | 131 | function page (props) { 132 | var date = dateFormat(Date(props.date), props.dateformat) 133 | 134 | return html` 135 | 139 |
140 | ${props.title} 141 |
142 |
143 | ` 144 | } -------------------------------------------------------------------------------- /src/views/page.js: -------------------------------------------------------------------------------- 1 | var html = require('choo/html') 2 | var raw = require('bel/raw') 3 | 4 | var wrapper = require('../components/wrapper-site') 5 | var NavPage = require('../components/nav-page') 6 | 7 | var titlebar = new NavPage() 8 | 9 | module.exports = wrapper(view) 10 | 11 | function view (state, emit) { 12 | var page = state.dropout.library[state.params.page] 13 | if (!page) return notfound({ basename: state.params.page }) 14 | 15 | // mark as read if not 16 | if (!page.read) { 17 | emit(state.events.DROPOUT_WRITEPAGE, { 18 | basename: state.params.page, 19 | read: true 20 | }) 21 | } 22 | 23 | return html` 24 |
25 | ${titlebar.render({ 26 | page: page, 27 | handleDelete: handleDelete 28 | })} 29 |
30 | ${raw(page.content)} 31 |
32 |
33 | ` 34 | 35 | function handleDelete (event) { 36 | emit(state.events.DROPOUT_DELETEPAGE, { basename: page.basename, redirect: '/' }) 37 | } 38 | } 39 | 40 | function notfound (basename) { 41 | return html` 42 |
43 | ${basename} not found 44 |
45 | ` 46 | } --------------------------------------------------------------------------------