summaryrefslogtreecommitdiff
path: root/desk/web/board
diff options
context:
space:
mode:
Diffstat (limited to 'desk/web/board')
-rw-r--r--desk/web/board/index.hoon456
-rw-r--r--desk/web/board/mobile-index.hoon128
-rw-r--r--desk/web/board/new.hoon299
-rw-r--r--desk/web/board/router.hoon281
-rw-r--r--desk/web/board/thread.hoon807
5 files changed, 1971 insertions, 0 deletions
diff --git a/desk/web/board/index.hoon b/desk/web/board/index.hoon
new file mode 100644
index 0000000..f70eb03
--- /dev/null
+++ b/desk/web/board/index.hoon
@@ -0,0 +1,456 @@
+/- boke, tp=trill-post, cnt=contact
+/+ sr=sortug, plib=trill-utils, sigil=sigil-sigil, lib=boke, kaji, const=constants, fetch-lib=fetch
+/= thread-preview /web/components/thread-preview
+|_ [s=state:boke =bowl:gall is-mobile=?]
++* fetch ~(. fetch-lib [s bowl])
+++ show-css ^~ %- trip
+'''
+.thread-preview{
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ border-bottom: 1px solid var(--text-color);
+ margin: auto;
+ padding: 0.4rem;
+ color: var(--text-color);
+
+ & .thread-author{
+ width: 10%;
+ & img, & svg {
+ width: 40px;
+ }
+ }
+
+ & .thread-name{
+ width: 60%;
+
+ & h2{
+ margin: 0;
+ color: var(--text-color);
+ }
+
+ & .thread-tags{
+ display: flex;
+ gap: 0.3rem;
+ & .tag{
+ padding: 0.2rem;
+ background-color: var(--huang);
+ opacity: 0.9;
+ font-size: 0.7rem;
+ cursor: pointer;
+ border-radius: 0.7rem;
+ }
+ }
+ }
+
+ & .reply-count{
+ width: 10%;
+ }
+ & .dates{
+ width: 20%;
+ text-align: right;
+ padding-right: 0.5rem;
+ & .last-reply-author{
+ display: flex;
+ gap: 1ch;
+ margin-left: auto;
+ width: fit-content;
+ }
+ }
+
+ &. reply-count{
+ font-size: 0.9rem;
+ }
+}
+@media (max-width: 768px){
+ .thread-preview{
+ & .meta{
+ justify-content: space-between;
+ }
+ & img, & svg{
+ display: none!important;
+ }
+ }
+}
+'''
+++ css ^~ %- trip
+'''
+body > main{
+ width: unset!important;
+}
+.tab:hover{
+ font-weight: 700;
+}
+.tab.active{
+ font-weight: 700;
+}
+#board-index{
+ width: 16%;
+ padding: 0 0.5rem;
+ border-right: 1px solid grey;
+
+ & header{
+ display: flex;
+ height: 60px;
+
+ & #select{
+ color: var(--hong);
+ }
+ & img{
+ width: 30px;
+ }
+ }
+}
+#board-list{
+ padding: 0.5rem;
+ height: calc(100% - 4rem);
+ & .entry.active{
+ border: 1px solid black; /* TODO */
+ }
+ & .entry{
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.5rem;
+ & img{
+ width: 30px;
+ margin-right: 10px;
+ }
+ & h3{
+ font-size: 30px;
+ margin: 0 10px 0 0;
+ }
+ & h4{
+ margin: 0;
+ }
+ }
+ & .entry:hover{
+ background-color: rgb(100, 100, 100, 0.3);
+ }
+}
+.tabs{
+ align-items: center;
+ justify-content: space-evenly;
+ width: 100%;
+}
+#board-proper{
+ width: 84%; /* TODO third column?*/
+
+ & .title{
+ padding: 1rem 2rem;
+ height: 60px;
+ border-bottom: 1px solid var(--text-color);
+ display: flex;
+ justify-content: space-between;
+
+ & h2{
+ text-transform: uppercase;
+ width: 30%;
+ margin: 0;
+ }
+ }
+ & #thread-list{
+ height: calc(100% - 60px);
+ overflow-y: scroll;
+ }
+ #sorting{
+ display: flex;
+ justify-content: space-evenly;
+ gap: 1rem;
+ align-items: center;
+ border-bottom: 1px solid var(--text-color);
+
+ & .sort-cat{
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ }
+ }
+}
+
+@media (max-width: 768px){
+ #board-main{
+ display: block;
+ & #board-index{
+ width: 100%;
+ }
+ & #board-proper{
+ display: none;
+ }
+ }
+}
+'''
+++ mobile-css ^~ %- trip
+'''
+@media (max-width: 800px){
+ #board-proper{
+ & .title{
+ padding: 0.5rem;
+ border-bottom: 1px solid var(--text-color);
+ & input{
+ width: 30%;
+ line-height: 2rem;
+ }
+ }
+ & #thread-list{
+ padding: 1rem;
+
+ & .thread-preview{
+ color: var(--text-color);
+ & h2{
+ margin-bottom: 0.2rem;
+ }
+ & .thread-inner{
+ border-bottom: 1px solid black;
+ margin-bottom: 0.5rem;
+ }
+ & .meta{
+ opacity: 0.7;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ }
+ }
+ }
+}
+.by-author{
+ display: flex;
+ gap: 1ch;
+}
+
+#tag-search{
+ margin-bottom: 1rem;
+}
+'''
+++ big-button-css ^~ %- trip
+'''
+.big-button{
+ display: flex;
+ background-color: var(--hong);
+ margin: auto;
+ position: fixed;
+ bottom: 50px;
+ left: 50%;
+ transform: translateX(-50%);
+ & svg{
+ width: 20px;
+ }
+}
+'''
+++ big-button
+;a.button.big-button/"/board/n"
+ ;style:"{big-button-css}"
+ ;svg
+ =viewBox "0 0 20 20"
+ =enable-background "new 0 0 20 20"
+ ;path
+ =fill "#FFFFFF"
+ =d "M17.561,2.439c-1.442-1.443-2.525-1.227-2.525-1.227L8.984,7.264L2.21,14.037L1.2,18.799l4.763-1.01l6.774-6.771l6.052-6.052C18.788,4.966,19.005,3.883,17.561,2.439z M5.68,17.217l-1.624,0.35c-0.156-0.293-0.345-0.586-0.69-0.932c-0.346-0.346-0.639-0.533-0.932-0.691l0.35-1.623l0.47-0.469c0,0,0.883,0.018,1.881,1.016c0.997,0.996,1.016,1.881,1.016,1.881L5.68,17.217z"
+ ;
+ ==
+ ;
+ ==
+ ;span:"New Thread"
+==
+
+++ recent-post
+|= [* =post:tp]
+:: =/ snippet (abbreviate-post:plib contents.post 100)
+=/ tags=marl %^ foldi:sr ~(tap in tags.post) `marl`~ |= [i=@ud tag=@t acc=marl]
+?: (gth i 3) acc
+:_ acc
+=/ tt (trip tag)
+;a@"/boards/tag/{tt}":"{tt}"
+
+;div.recent-post
+ ;div.author:"{(scow %p author.post)}"
+ ;div.tags
+ ;* tags
+ ==
+==
+++ main
+|= post-list=manx
+^- manx
+;div#board-main.flex.fh
+;style: {css}
+;style:"{show-css}"
+ ;div#board-index.fh
+ ;header
+ ;div.tabs :: Boards/tags/latest
+ ;div.tab.active
+ =kaji "scry"
+ =path "/board/f/boards"
+ =swap "swap"
+ =targ "#board-list"
+ ; Boards
+ ==
+ ;div.tab
+ =kaji "scry"
+ =path "/board/f/tags"
+ =swap "swap"
+ =targ "#board-list"
+ ; Tags
+ ==
+ ==
+ ==
+ ;div#board-list.fsy
+ ;* category-list
+ ==
+ ==
+ ;+ post-list
+==
+++ category-list ^- marl
+ :- ;div.entry
+ =kaji "scry"
+ =swap "swap"
+ =path "/board/f/page/l/l/0"
+ =targ "#board-proper"
+ :: ;img@"https://s3.spandrell.ch/assets/board/boards/{name}.svg";
+ ;h3: 全
+ ;h4: All
+ ==
+ ;* %+ turn ~(tap by categories:const) |= [p=@tas *]
+ =/ name (trip p)
+ ;div.entry
+ =kaji "scry"
+ =swap "swap"
+ =path "/board/f/page/b/{name}/0"
+ =targ "#board-proper"
+ ;img@"https://s3.spandrell.ch/assets/board/boards/{name}.svg";
+ ;h4: {name}
+ ==
+++ make-tag-list
+ |= l=(list [@t @ud]) ^- marl
+ ;= ;+ tag-search-input
+ ;+ ?~ l
+ ;span:"No tags found"
+ ;div#board-list-inner.tags
+ ;* %+ turn l tag-preview
+ ==
+ ==
+++ tag-search-input
+;div#tag-search
+ ;input(type "text", name "query", placeholder "Search tag")
+ =kaji "search"
+ =bounce "1000"
+ =path "/board/f/tag-search"
+ =swap "swap"
+ =targ "#board-list-inner"
+ ;
+ ==
+==
+++ tag-preview
+ |= [t=@t c=@ud] ^- manx
+ =/ name (trip t)
+ =/ count (scow:parsing:sr %ud c)
+ ?. is-mobile
+ ;div.entry
+ =kaji "scry"
+ =swap "swap"
+ =path "/board/f/page/t/{name}/0"
+ =targ "#board-proper"
+ ;h4:"{name} ({count})"
+ ==
+ ;a.entry/"/board/t/{name}"
+ ;h4:"{name} ({count})"
+ ==
+
+++ cursor
+|= [cursor=(unit @da) label=tape =rq search=(unit @t)] ^- manx
+?~ cursor ;div;
+=/ type
+ ?- -.rq
+ %latest "l/l"
+ %board "b/{(trip name.rq)}"
+ %tag "t/{(trip name.rq)}"
+ ==
+=/ path ?~ search
+"/board/f/page/{type}/{(scow:parsing:sr %uw u.cursor)}"
+"/board/f/search/{type}/{(scow:parsing:sr %uw u.cursor)}?query={(trip u.search)}"
+
+;div.cursor(kaji "iscroll", path path, cont "#thread-list", where "bottom")
+ ;button:"{label} posts"
+==
++$ rq $%([%latest ~] [%board name=@t] [%tag name=@t])
+++ post-index
+|= [=tpage:tp =rq contacts=whoms:cnt search=(unit @t) is-mobile=?] ^- manx
+~& > cursors=[newer.tpage older.tpage]
+=/ title ?- -.rq
+ %latest "Latest posts"
+ %board (trip +.rq)
+ %tag "Tag: {(trip +.rq)}" ==
+:: =/ cursor-string ?~ older.page "0" (scow:parsing:sr %uw u.older.page)
+=/ cursor-string ""
+~& page-req=+.page
+:: =/ order "Post date" :: || "Reply date"
+=/ flop-path "/board/f/flop/"
+=/ search-path ?- -.rq
+ %latest "/board/f/search/l/l/{cursor-string}"
+ %board "/board/f/search/b/{(trip +.rq)}/{cursor-string}"
+ %tag "/board/f/search/t/{(trip +.rq)}/{cursor-string}" ==
+=/ sort-path ?- -.rq
+ %latest "/board/f/page/h/l/l/0"
+ %board "/board/f/page/b/{(trip +.rq)}/0"
+ %tag "/board/f/page/t/{(trip +.rq)}/0" ==
+
+;div#board-proper.fh
+ ;style: {mobile-css}
+ ;div.title.f1
+ :: ;button
+ :: =kaji "scry"
+ :: path flop-path
+ :: ; Flop
+ :: ==
+ ;h2:"{title}"
+ ;input(type "text")
+ =placeholder "Search threads"
+ =kaji "search"
+ =name "query"
+ =bounce "1000"
+ =path search-path
+ =swap "swap"
+ =targ "#thread-list"
+ ;
+ ==
+ ==
+ :: ;div#sorting
+ :: =kaji "scry"
+ :: =swap "swap"
+ :: =targ "thread-list"
+
+ :: ;div.sort-cat
+ :: ;span:"Title"
+ :: ;span(path "{sort-path}?sort=title&dir=asc"):"⇑"
+ :: ;span(path "{sort-path}?sort=title&dir=desc"):"⇓"
+ :: ==
+ :: ;div.sort-cat
+ :: ;span:"Author"
+ :: ==
+ :: ;div.sort-cat
+ :: ;span:"Posted"
+ :: ==
+ :: ;div.sort-cat
+ :: ;span:"Last Reply"
+ :: ==
+ :: ;div.sort-cat
+ :: ;span:"Reply Count"
+ :: ==
+ :: ==
+ ;div#thread-list
+ ;* %+ turn p.tpage |= t=thread:tp
+ =/ preview ~(. thread-preview [t contacts bowl])
+ ?: is-mobile mobile:preview wide:preview
+ ;+ (cursor older.tpage "Older" rq search)
+ ==
+ ;+ big-button
+==
+++ previews
+|= [=tpage:tp =rq contacts=whoms:cnt search=(unit @t) is-mobile=?] ^- manx
+;div
+ ;* %+ turn p.tpage |= t=thread:tp
+ =/ preview ~(. thread-preview [t contacts bowl])
+ ?: is-mobile mobile:preview wide:preview
+ ;+ (cursor older.tpage "Older" rq search)
+==
+--
diff --git a/desk/web/board/mobile-index.hoon b/desk/web/board/mobile-index.hoon
new file mode 100644
index 0000000..3c57173
--- /dev/null
+++ b/desk/web/board/mobile-index.hoon
@@ -0,0 +1,128 @@
+/- boke, tp=trill-post, cnt=contact
+/+ sr=sortug, plib=trill-utils, sigil=sigil-sigil, lib=boke, kaji, const=constants
+/= board /web/board/index
+|_ [s=state:boke =bowl:gall]
+++ css ^~ %- trip
+'''
+#board-index{
+ margin-top: 1rem;
+ .tabs{
+ width: 50%;
+ margin: auto;
+
+ & .tab.active{
+ font-weight: 700;
+ }
+ }
+ & #board-list{
+ input{
+ display: block;
+ margin: auto;
+ }
+ }
+ & #board-list-inner {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+
+ & .board{
+ display: block;
+ cursor: pointer;
+ padding: 1rem 0;
+ border: 1px solid var(--text-color);
+ margin: 0.3rem;
+ width: 20%;
+ text-align: center;
+ & .hanzi{
+ font-size: 50px;
+ margin: 0;
+ color: var(--text-color);
+ }
+ & h4{
+ word-break: keep-all;
+ text-align: center;
+ }
+
+ & img{
+ width: 50px;
+ display: block;
+ margin: 0 auto 0.5rem auto;
+ }
+ }
+ & .entry{
+ display: flex;
+ flex: 0 0 33%;
+ align-items: center;
+ padding: 0.5rem 1rem;
+
+ & h4{
+ margin-right: 0.2rem;
+ }
+ }
+ }
+
+}
+#board{
+ display: block;
+}
+ #board-index{
+ .board{
+ width: unset;
+ }
+ & h4{
+ font-size: 1rem;
+ }
+ }
+ #tag-search{
+ margin-bottom: 1rem;
+ }
+'''
+++ $
+:: =/ latest (bot:gorrm:tp feed.s 10)
+;div#board-index
+ ;style: {css}
+ ;h2.tc:"Forum"
+ ;div.tabs
+ :: ;div.tab
+ :: =path "/board/latest"
+ :: ; Latest
+ :: ==
+ ;div.tab.active
+ =kaji "scry"
+ =swap "swap"
+ =targ "#board-list-inner"
+ =path "/board/ff/boards"
+ ; Categories
+ ==
+ ;div.tab
+ =kaji "scry"
+ =swap "swap"
+ =targ "#board-list"
+ =path "/board/ff/tags"
+ ; Tags
+ ==
+ ==
+ ;div#board-list
+ ;div#board-list-inner
+ ;* category-list
+ ==
+ ==
+ ;+ big-button:board
+==
+++ category-list ^- marl
+ :-
+ ;a.board/"/board/all"
+ ;div
+ ;h3.hanzi: 全
+ ;h4: All
+ ==
+ ==
+ ;* %+ turn ~(tap by categories:const) |= [p=@tas *]
+ =/ name (trip p)
+ ;a.board/"/board/b/{name}"
+ ;div
+ ;img@"https://s3.spandrell.ch/assets/board/boards/{name}.svg";
+ ;h4: {name}
+ ==
+ ==
+--
diff --git a/desk/web/board/new.hoon b/desk/web/board/new.hoon
new file mode 100644
index 0000000..ed71a91
--- /dev/null
+++ b/desk/web/board/new.hoon
@@ -0,0 +1,299 @@
+/- *boke, cnt=contact, tp=trill-post
+/+ ui=trill-ui, kaji, const=constants, wall
+/= user /web/components/user
+|_ [tt=tags-table =bowl:gall edit=(unit [thread:tp post:tp])]
+++ css ^~ %- trip
+'''
+#error-div{
+ color: red;
+}
+#new-thread{
+ padding: 10px;
+ margin: auto;
+ border: 1px solid var(--background-color);
+ border-radius: 1%;
+
+ & fieldset{
+ border: none;
+ padding: 0;
+ margin-top: 1rem;
+
+ & label{
+ display: block;
+ }
+ & input{
+ width: 100%;
+ line-height: 1.5rem;
+ }
+ }
+
+ & .board-choice{
+ display: flex;
+ align-items: center;
+
+ & .left{
+ width: 20%;
+ & select{
+ background-color: var(--background-color);
+ height: 2rem;
+ }
+ }
+ & .right{
+ margin-left: 2rem;
+ flex-grow: 1;
+ }
+ }
+ & .author{
+ img{
+ width:30px;
+ height:30px;
+ }
+ & .name{
+ margin-left: 1rem;
+ }
+ }
+ & #bordered{
+ border: 1px solid black;
+ width: 100%;
+ height: 50vh;
+ overflow-y: scroll;
+ & textarea{
+ resize: none;
+ width: 100%;
+ height: 90%;
+ border: none;
+ outline: none;
+ }
+ }
+ & #chips{
+ position: relative;
+ width: 100%;
+ border: 1px solid rgb(107, 114, 128);
+ padding: 0 0.5rem;
+
+ & .chip{
+ display: inline-block;
+ font-size: 13px;
+ font-weight: 500;
+ color: rgba(0, 0, 0, 0.6);
+ line-height: 26px;
+ padding: 0 12px;
+ border-radius: 16px;
+ background-color: #e4e4e4;
+ cursor: pointer;
+ margin: 0 2px;
+ }
+ & .selected-chip{
+ background-color: rgb(100, 100, 100);
+ }
+ & .chip:hover{
+ background-color: rgb(100, 100, 100);
+ }
+
+ & input{
+ border: 0;
+ display: inline-block;
+ font-size: 16px;
+ height: 2rem;
+ line-height: 32px;
+ outline: 0;
+ margin: 0;
+ padding: 0 !important;
+ }
+ }
+}
+@media (min-width: 800px){
+ #new-thread{
+ width: 70%;
+ }
+
+}
+@media (max-width: 800px){
+
+}
+.buttons{
+ display: flex;
+ gap: 0.5rem;
+ justify-content: end;
+ align-items: center;
+
+}
+#poll-button{
+ border: none;
+ background: url('https://s3.spandrell.ch/assets/board/ui/poll.svg');
+ background-size: cover;
+ width: 30px;
+ height: 30px;
+}
+'''
+++ script
+=/ json-tags=json %- pairs:enjs:format
+ %+ turn ~(tap by tt) |= [tag=@t p=(list *)]
+ :+ tag %n (scot %ud (lent p))
+=/ json-string %- trip %- en:json:html json-tags
+^~ %+ weld
+"""
+const jsonString = '{json-string}';
+const allTags = JSON.parse(jsonString);
+
+"""
+%- trip
+'''
+console.log(allTags, "tags")
+const tagInput = document.getElementById("tag-input");
+const tagStore = document.getElementById("tag-store");
+const chipContainer = document.getElementById("chips");
+tagInput.addEventListener("keypress", handleKey);
+tagInput.addEventListener("keyup", handleBackspace);
+
+const chipList = new Set();
+
+function init(){
+ setTimeout(() => {
+ console.log(tagStore.value, "tagstore")
+ const initTags = tagStore.value.split(",");
+ for (let tag of initTags){
+ if (tag){
+ chipList.add(tag);
+ loadChip(tag);
+ }
+ }
+ }, 500)
+}
+init();
+
+function chipString(){
+ const string = [...chipList].reduce((acc, i) => `${acc},${i}`);
+ // const string = JSON.stringify(Array.from(chipList));
+ tagStore.value = string;
+}
+function addChip(itag){
+ const tag = itag.toLowerCase();
+ if (!chipList.has(tag) && tag.length > 0 && tag.length < 32) {
+ chipList.add(tag);
+ this.loadChip(tag)
+ tagInput.value = "";
+ chipString();
+ }
+}
+function removeChip(e){
+ chipList.delete(e.target.innerText);
+ chipContainer.removeChild(e.target);
+ chipString();
+}
+function loadChip(tag) {
+ const c = document.createElement('div');
+ c.classList.add("chip");
+ c.id = tag;
+ c.innerText = tag;
+ c.addEventListener("click", removeChip)
+ chipContainer.insertBefore(c, tagInput);
+}
+function handleBackspace(e){
+ console.log(e.keyCode, "keyup")
+ console.log(e.target.selectionStart, "keyup")
+ if (!(e.keyCode === 8 && e.target.selectionStart == 0)) return
+ const chips = chipContainer.querySelectorAll('.chip');
+ if (chips.length === 0) return
+ const to_delete = chipContainer.querySelector('.selected-chip');
+
+ if (to_delete) removeChip({target: to_delete});
+ else {
+ const last_chip = chips[chips.length - 1];
+ last_chip.classList.add("selected-chip");
+ }
+}
+function handleKey(e) {
+ // autocomplete?
+ console.log(e.keyCode, "keycode")
+ console.log(e)
+ if (e.keyIdentifier == 'U+000A' || e.keyIdentifier == 'Enter' || e.keyCode == 13) e.preventDefault();
+ if (e.keyCode === 13) addChip(e.target.value);
+}
+
+
+'''
+++ $ ^- manx
+=/ [stitle=tape stags=tape stext=tape] ?~ edit ["" "" ""]
+:+ (trip title.-.u.edit)
+ (tags-to-tape:ui tags.-.u.edit)
+ (content-to-md:ui contents.+.u.edit)
+=/ wal ~(. wall src.bowl)
+=/ cats boards:wal
+=/ whoms (get-contacts:cnt bowl)
+=/ usr (user src.bowl whoms 30)
+=/ subscription (subscription-type:wal now.bowl)
+;div#new-thread.fsy
+ ;style:"{css}"
+ ;+ ?~ edit
+ ;h1.tc:"New Thread"
+ ;h1.tc:"Edit Thread"
+ ;form
+ =kaji "poke"
+ =action "add-thread"
+ ;fieldset
+ ;label:"Title"
+ ;input(type "text", name "title", value stitle);
+ ==
+ ;fieldset.board-choice
+ ;label.left
+ ;div:"Board"
+ ;select
+ =name "board"
+ ;* %+ turn %- sort :_ aor ~(tap in cats) |= nam=@t
+ =/ name (trip nam)
+ ?: .=(nam 'public')
+ ;option(value name, selected ""):"{name}"
+ ;option(value name):"{name}"
+ ==
+ ==
+ ;label.right
+ ;div:"Tags"
+ ;div#chips
+ ;input(type "text", placeholder "press enter to input individual tags, click on a tag to delete", enterkeyhint "enter", id "tag-input");
+ ;input(type "hidden", id "tag-store", name "tags", value stags);
+ ==
+ ==
+ ==
+ ;fieldset
+ ;label:"Text"
+ ;div#bordered
+ ;textarea(name "text")
+ ; {stext}
+ ==
+ ==
+ ==
+ ;div.f1
+ ;div.author.flex
+ ;div:"Posting as"
+ ;div.flex.ml1
+ ;+ avatar.usr
+ ;+ name.usr
+ ==
+ ==
+ ;input(type "hidden", name "error-div", value "#error-div");
+ ;span#error-div;
+ ;div.buttons
+ ;* ?~ subscription ~
+ ;= ;button#poll-button(type "button")
+ =kaji "scry"
+ =swap "add"
+ =cont "#bordered"
+ =path "/board/f/poll/new"
+ ;
+ ==
+ ;input#upload-img@"https://s3.spandrell.ch/assets/board/octicons/image-24.svg"(type "image");
+ ==
+ ;* ?~ edit ;+ ;input(type "submit", value "Submit");
+ ;= ;input(type "submit", value "Edit");
+ ;input(type "hidden", name "editing")
+ =value (enc:kaji pid.-.u.edit)
+ ;
+ ==
+ ==
+ ==
+ ==
+ ==
+ ;script:"{script}"
+==
+--
diff --git a/desk/web/board/router.hoon b/desk/web/board/router.hoon
new file mode 100644
index 0000000..48c72a9
--- /dev/null
+++ b/desk/web/board/router.hoon
@@ -0,0 +1,281 @@
+/- boke, cnt=contact, tp=trill-post
+/+ kaji, fetch-lib=fetch, plib=trill-utils, const=constants, sr=sortug, lib=boke, ui=trill-ui
+/= index /web/index
+/= board /web/board/index
+/= mobile /web/board/mobile-index
+/= thread-page /web/board/thread
+/= new-thread /web/board/new
+/= nag /web/nag
+/= poll /web/components/poll
+
+|_ [rl=req-line:kaji s=state:boke =bowl:gall]
+ +* fetch ~(. fetch-lib [s bowl])
+ b ~(. board [s bowl mob.rl])
+ ::
+ ++ eyre-bail (error-response:kaji 404)
+ ++ manx-bail (error-page:kaji 404)
+::
+++ $ ^- eyre-res:kaji
+ =/ p ?. mob.rl pat.rl [%m pat.rl]
+
+ ?: ?=([%m ~] p) [%page (index ~[(mobile s bowl)] bowl)]
+ ?: ?=([%m %ff rest=*] p) [%html (mobile-fragment rest.p)]
+ =. p ?: ?=([%m rest=*] p) ((pole knot) rest.p) p
+ ?: ?=([%f rest=*] p) [%html (fragment rest.p)]
+ :- %page
+ %- index :_ bowl :_ ~
+ ?+ p manx-bail
+ :: boards
+ ~ (main par.rl)
+ :: these two mostly for mobile
+ [%t tag=@t ~] (tag-index tag.p)
+ [%b cat=@t ~] (board-index cat.p)
+ [%all ~] all-posts
+ :: threads
+ [%n ~] (new-thread tags.s bowl ~)
+ [%edit uid=@t ~] (edit-thread uid.p)
+ [%blog rest=*] (blog-as-thread rest.p)
+ [%p %esc uid=@t ~] (board-thread-no-path uid.p)
+ [tag=@t title=@t rest=*] (board-thread tag.p title.p rest.p)
+ :: [%p rest=*] (board-thread rest.p)
+ ==
+++ fragment
+ |= p=(pole knot)
+ ?+ p ~& no-fragment=p manx-bail
+ :: fetching categories
+ [%boards ~] (w category-list:b)
+ [%tags ~] (w (make-tag-list:b (scag 21 (tags-by-size:lib tags.s src.bowl))))
+ [%tag-search ~] (w tag-search)
+ :: fetching posts
+ [%page type=@t name=@t cursor=@t ~] (post-page type.p name.p cursor.p *_par.rl)
+ [%search type=@t name=@t cursor=@t ~] (post-page type.p name.p cursor.p par.rl)
+ ::
+ :: fetching replies of thread
+ [%replies thread=@t cursor=@t ~] (thread-replies thread.p cursor.p)
+ [%snip uid=@t ~] (post-snippet uid.p)
+ [%edit uid=@t ~] (edit-box uid.p)
+ [%poll %new ~] new-poll
+ ==
+++ mobile-fragment
+ |= p=(pole knot)
+ ?+ p manx-bail
+ [%boards ~] (w category-list:mobile)
+ [%tags ~] (w (make-tag-list:b (scag 33 (tags-by-size:lib tags.s src.bowl))))
+ [%tag-search ~]
+ =/ uq (~(get by par.rl) 'query')
+ %- w
+ ;* ?~ uq :~(;span:"No query")
+ %+ turn (tag-search:fetch u.uq) tag-preview:b
+ [%snip uid=@t ~] (post-snippet uid.p)
+ [%edit uid=@t ~] (edit-box uid.p)
+ ==
+
+
+
+++ w wrap-marl:kaji
+:: Boards
+++ main
+|= params=(map @t @t)
+ =/ post-list
+ =/ tag (~(get by params) 't')
+ ?^ tag (post-page 't' u.tag '0' ~)
+ =/ brd (~(get by params) 'b')
+ ?^ brd (post-page 'b' u.brd '0' ~)
+ (post-page '' '' '0' ~)
+
+ (main.b post-list)
+
+++ tag-search ^- marl
+ =/ uq (~(get by par.rl) 'query')
+ ;* ?~ uq :~(;span:"No query")
+ %+ turn (tag-search:fetch u.uq) tag-preview:b
+
+
++$ rq $%([%latest ~] [%board name=@t] [%tag name=@t])
+
+++ tag-index
+|= tag=@t
+ %- post-page
+ :^ 't' tag '0' ~
+++ board-index
+|= cat=@t
+ %- post-page
+ :^ 'b' cat '0' ~
+++ all-posts
+ %- post-page
+ :^ 'l' '' '0' ~
+
+++ post-page
+ |= [stype=@t name=@t scursor=@t params=(map @t @t)]
+ =/ contacts (get-contacts:cnt bowl)
+ =/ older=(unit @) ?: .=('0' scursor) ~ (slaw:parsing:sr %uw scursor)
+ =/ =rq
+ ?: .=(stype 't') [%tag name]
+ ?: .=(stype 'b') [%board name]
+ [%latest ~]
+
+ =/ query (~(get by params) 'query')
+ =/ sort-by (~(get by params) 'sort')
+ =/ page-req [~ older board-page-size:const]
+ =/ filter ?~ query
+ %- some |= =thread:tp
+ !(~(has in tags.thread) 'blog')
+ %- some |= =thread:tp
+ ?: (~(has in tags.thread) 'blog') .n
+ (cfind:sr u.query title.thread .n)
+ =/ =tpage:tp
+ ?- -.rq
+ %latest (active-thread-page-all:fetch page-req filter)
+ %tag (active-thread-page-by-tags:fetch ~[name] page-req filter)
+ %board (active-thread-page-by-board:fetch name page-req filter)
+ ==
+ ?: .=('0' scursor)
+ (post-index:b tpage rq contacts query mob.rl)
+ (previews:b tpage rq contacts query mob.rl)
+:: Threads
+
+++ edit-thread |= uid=@t
+ =/ op (thread-by-hash:fetch uid)
+ ?~ op manx-bail
+ (new-thread tags.s bowl op)
+++ board-thread-no-path
+ |= uid=@t
+ =/ post (post:fetch uid)
+ ?~ post manx-bail
+ (serve-thread u.post)
+
+++ blog-as-thread
+ |= p=path
+ =/ op (by-path:fetch p)
+ ?~ op ~& board-blog-path-not-found=p manx-bail
+ =/ is-op .=(thread.u.op [author.u.op id.u.op])
+ ?. is-op manx-bail (serve-thread u.op)
+
+++ serve-thread
+|= =post:tp
+ =/ ted (get:torm:tp threads.s thread.post)
+ ?~ ted ~& board-ted-not-found=rl manx-bail
+ :: =/ fn (node-to-full:plib post feed.s)
+ =/ contacts (get-contacts:cnt bowl)
+ :: =/ post-pid=pid:tp [author.u.post id.u.post]
+ :: ?~ parent.post
+ =/ =cpage:tp (thread-children:fetch u.ted)
+ =. p.cpage [post p.cpage]
+ ~& >> cpage=+.cpage
+ (thread-page u.ted cpage ~ ~ contacts s bowl mob.rl)
+++ thread-replies
+|= [ts=@t cs=@t]
+ =/ contacts (get-contacts:cnt bowl)
+ =/ thread (dec:kaji ts pid:tp)
+ ?~ thread manx-bail
+ =/ cursor (dec:kaji cs @ud)
+ ?~ cursor manx-bail
+ =/ ted (get:torm:tp threads.s u.thread)
+ ?~ ted manx-bail
+ ?~ replies.u.ted manx-bail
+ =/ =cpage:tp (older-children:fetch u.ted +(u.cursor))
+ =< inline
+ %= thread-page
+ thread u.ted
+ children cpage
+ focus (some i.replies.u.ted)
+ search ~
+ contacts contacts
+ state s
+ bowl bowl
+ is-mobile mob.rl
+ ==
+
+++ board-thread
+ |= [tag=@t title=@t rest=(pole knot)]
+ =/ post (by-path:fetch [tag title ~])
+ ?~ post ~& board-path-not-found=rl cant-show
+ (serve-thread u.post)
+
+ ++ cant-show html:nag
+
+ :: =/ children (flatten-fn-2:plib fn)
+ :: =/ req [~ ~ thread-page-size:const]
+ :: =/ children (get-children-page:plib fn req)
+ :: ~& children=+.children
+
+ :: =/ par (get:gorm:tp feed.s u.parent.post)
+ :: ?~ par ~& par-not-found=rl
+ :: =/ children (bulk-post-by-pid:fetch replies.u.ted)
+ :: =/ =spage:tp [children ~ ~ (lent children)]
+ :: (thread-page title.u.ted contents.u.post spage ~ ~ contacts bowl mob.rl)
+ :: =/ siblings (get-siblings-page-2:plib u.par)
+ :: (thread-page title.u.ted contents.u.post siblings `post-pid ~ contacts bowl mob.rl)
+
+++ edit-box
+ |= uid=@t
+ =/ up (post:fetch uid)
+ ?~ up not-found
+ =/ tag-string (tags-to-tape:ui tags.u.up)
+ =/ post-text (content-to-md:ui contents.u.up)
+ =/ action ?~ parent.u.up "add-post" "add-reply"
+ ;form.content.edit-box
+ =kaji "poke"
+ =action action
+ ;input(type "hidden", name "editing", value (trip uid));
+ ;input(type "hidden", name "error-div", value ".error-div");
+ ;input(type "hidden", name "title", value (trip title.u.up));
+ ;* ?~ parent.u.up ~ =/ p-string (enc:kaji [thread.u.up u.parent.u.up])
+ ;+ ;input(type "hidden", name "parents", value p-string);
+ ;textarea
+ =name "text"
+ ; {post-text}
+ ==
+ :: ;label
+ :: ; Tags
+ :: ;input(type "text", name "tags", value tag-string);
+ :: ==
+ ;div.buttons
+ ;input
+ =type "submit"
+ =value "Save"
+ ;
+ ==
+ ;button
+ =kaji "poke"
+ =action "del-reply"
+ =name "pid"
+ =payload (trip uid)
+ ; Delete
+ ==
+ ;div.error-div;
+ ==
+ ==
+++ post-snippet
+ |= uid=@t
+ =/ up (post:fetch uid)
+ ?~ up not-found
+ =/ contacts (get-contacts:cnt bowl)
+ =/ t thread-page(bowl bowl, state s, contacts contacts)
+ (snippet:t u.up)
+
+
+++ not-found
+;div
+ ; Post not found
+==
+
+++ new-poll (form:poll bowl)
+
+
+:: ++ serve-tags
+:: |= tags=path
+:: =/ postl (search-filter-full:plib feed.s [~ ~ board-page-size:const] (tag-search:plib tags))
+:: =/ tag-string %^ foldi:sr tags "" |= [i=@ud t=@t acc=tape]
+:: ?: .=(i (dec (lent tags)))
+:: "{acc}{(trip t)}"
+:: "{acc}{(trip t)}, "
+:: =/ page
+:: ;div#tag-search
+:: ;header
+:: ;h2.tc: Tag Search: {tag-string}
+:: ==
+:: ;+ (html:post-list postl)
+:: ==
+:: (index ~[title page] bowl)
+--
diff --git a/desk/web/board/thread.hoon b/desk/web/board/thread.hoon
new file mode 100644
index 0000000..98811f8
--- /dev/null
+++ b/desk/web/board/thread.hoon
@@ -0,0 +1,807 @@
+/- boke, tp=trill-post, cons=contact
+/+ sr=sortug, plib=trill-utils, ui=trill-ui, sigil=sigil-sigil, lib=boke, kaji, constants, wall
+/= post-text /web/components/post-text
+/= user /web/components/user
+|_ [=thread:tp children=cpage:tp focus=(unit pid:tp) search=(unit @t) contacts=whoms:cons =state:boke =bowl:gall is-mobile=?]
++* wal ~(. wall src.bowl)
+ pt ~(. post-text [state bowl])
+++ css ^~ %- trip
+'''
+body>main{
+ width: 100%!important;
+}
+@media (max-width: 800px){
+ body>main{
+ padding: 0!important;
+ }
+}
+#board-thread{
+ max-width: 1280px;
+ margin: auto;
+
+/* base styles */
+ & header{
+ margin-top: 1rem;
+ & img{
+ width: 20px;
+ margin-left: 1rem;
+ }
+ }
+
+ & p{
+ font-size: 1.1rem;
+ }
+
+}
+
+.reply{
+ display: grid;
+ border: 1px solid silver;
+
+ & .author{
+
+ & .avatar{
+ max-height: 100px;
+ max-width: 64px;
+ display: block;
+ margin: auto;
+ }
+ & .name{
+ font-weight: 500;
+ color: var(--hong);
+ text-align: center;
+ word-break: break-word;
+ }
+ }
+
+ & .metadata {
+ & p{
+ margin: -10px 0 0 0;
+ font-size: 14px;
+ }
+ & .timestamps span{
+ margin-right: 0.3rem;
+ }
+ & .buttons{
+ display: flex;
+ & img{
+ height: 20px;
+ width: 20px;
+ margin: 5px 5px;
+ cursor: pointer;
+ }
+ }
+
+ }
+
+ & .content{
+ text-align: left:
+ word-wrap: break-word;
+
+ & img{
+ max-width: 100%;
+ }
+
+ & p{
+ padding: 0.5rem;
+ font-weight: 400;
+ overflow: hidden;
+ word-break: break-word;
+ white-space: normal;
+ margin: 1rem 0;
+ }
+ & blockquote{
+ margin: 1rem 2rem 1rem 1rem;
+ padding: 0.4rem;
+ border: 1px solid grey;
+ border-radius: 2%;
+ }
+ & .youtube-frame{
+ display: block;
+ margin: auto;
+ width: 70%;
+ min-height: 30vw;
+ }
+ }
+ & footer{
+ width: 100%;
+ padding: 0 0.5rem;
+ .tags{
+ padding: 0;
+ font-size: 0.9rem;
+ flex-wrap: wrap;
+
+ a{
+ margin-left: 0.1rem;
+ }
+ }
+ }
+
+}
+
+
+
+ /* desktop */
+
+ @media (min-width: 1000px){
+ #posts{
+ display: flex;
+ }
+ #replies{
+ width: 83.33%;
+ }
+ }
+ @media(min-width: 800px){
+
+ #sidebar{
+ position: sticky;
+ top: 4rem;
+ right: 0;
+ align-self: flex-start;
+
+ & button, & .button{
+ display: block;
+ margin: 1rem auto;
+ width: 100%;
+ }
+ }
+
+ .reply{
+ padding: 1rem;
+ padding-left: 0.4rem;
+ margin: 1rem;
+ grid-template-columns: 10% 90%;
+ grid-template-rows: 40px auto;
+
+ & .author{
+ grid-column-start: 1;
+ grid-column-end: 2;
+ grid-row-start: 1;
+ grid-row-end: 3;
+ }
+
+ & .metadata{
+ display: flex;
+ justify-content: space-between;
+ border-bottom: 1px solid grey; /* TODO darkmode */
+ grid-column-start: 2;
+ grid-column-end: 3;
+ grid-row-start: 1;
+ grid-row-end: 2;
+ }
+
+ & .content{
+ grid-column-start: 2;
+ grid-column-end: 3;
+ grid-row-start: 2;
+ grid-row-end: 3;
+
+ & img{
+ margin: 0 auto;
+ max-height: 300px;
+ }
+ }
+ }
+ }
+ /* mobile */
+ @media(max-width: 800px){
+ & header{
+ padding: 0 1rem;
+ & h1{
+ font-size: 1.8rem;
+ }
+ }
+ #replies{
+ margin-bottom: 3rem;
+ }
+
+ #posts{
+ display: block;
+ width: 100%;
+
+ & > footer{
+ background-color: black;
+ display:flex;
+ justify-content: space-evenly;
+ width: 100vw;
+ position: fixed;
+ bottom: 0;
+ height: 3rem;
+
+ & button{
+ height: 3rem;
+ }
+ }
+ & .replies{
+ margin-bottom: 3rem;
+ }
+ }
+
+ .reply{
+ padding-top: 10px;
+ grid-template-columns: 100px auto;
+ grid-template-rows: 80px auto;
+
+ & .author{
+ grid-column-start; 1;
+ grid-column-end: 2;
+ grid-row-start: 1;
+ grid-row-end: 2;
+
+ & .avatar{
+ height: 60px;
+ width: 60px;
+ display: block;
+ margin: 0 auto;
+ }
+ & .name{
+ font-size: 0.9rem;
+ }
+ }
+ & .metadata{
+ grid-column-start: 2;
+ grid-column-end: 3;
+ grid-row-start: 1;
+ grid-row-end: 2;
+ padding: 0 0.2rem;
+ border-bottom: 1px solid grey;
+
+ /* & .date{
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }*/
+ .updated-date{
+ display: none;
+ }
+
+ & .buttons{
+ width: 100%;
+ justify-content: space-between;
+ margin-top: 0.5rem;
+ }
+ }
+ & .content{
+ grid-column-start: 1;
+ grid-column-end: 3;
+ grid-row-start: 2;
+ grid-row-end: 3;
+
+ & img{
+ max-height: 300px;
+ }
+
+ }
+ }
+ }
+
+
+}
+
+#page-nav{
+ .button{
+ width: 30px;
+ height: 30px;
+ }
+}
+.icon-button{
+ padding: 0.2rem;
+ & img{
+ width: 20px;
+ }
+}
+.replying{
+ margin: 1rem;
+ padding: 0.5rem;
+ width: fit-content;
+ cursor: pointer;
+ background-color: rgb(100, 100, 100, 0.3);
+ border-radius: 0.5rem;
+ & span{
+ margin: 0 0.4rem;
+ }
+}
+.edit-box{
+ display: block;
+ & textarea{
+ display: block;
+ width: 100%;
+ height: 200px;
+ outline: none;
+ resize: none;
+ }
+ & label{
+ margin-right: 1rem;
+ }
+}
+.reply-box{
+ & .bordered{
+ border: 1px solid black;
+ & blockquote{
+ margin:0;
+ padding: 0.3rem;
+ border-bottom: 1px solid grey;
+ & .content{
+ color: green;
+ white-space: pre-wrap;
+ }
+ & .content::before{
+ content: "> ";
+ }
+ }
+ & .quoting-author{
+ display: flex;
+ gap: 1ch;
+ }
+ & textarea{
+ resize: none;
+ display: block;
+ border: none;
+ outline: none;
+ width: 100%;
+ }
+ }
+ & .reply-buttons{
+ margin-top: 0.8rem;
+ display: flex;
+ gap: 0.5rem;
+ justify-content: end;
+ align-items: center;
+
+ & input[type=image]{
+ width: 30px;
+ }
+
+ }
+}
+/* poll */
+/* base */
+
+.poll{
+ border: 1px solid black;
+ margin: 1rem auto;
+ & .bottom-row{
+ height: 1rem;
+ }
+ & .top-row{
+ position: relative;
+ height: 2rem;
+ & h6{
+ position: absolute;
+ left: 1%;
+ font-size: 1.1rem;
+ }
+ & h3{
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 1.4rem;
+ max-width: 65%;
+ }
+ }
+ & .poll-type{
+ font-size: 0.7rem;
+ float: left;
+ }
+ & .expiry{
+ font-size: 0.8rem;
+ float: right;
+ }
+ & .poll-option{
+ position: relative;
+ border: 2px solid grey;
+ width: 80%;
+ height: 2rem;
+ margin: 0.3rem auto 0.3rem auto;
+ & .inner{
+ padding: 0 0.5rem;
+ z-index: 20;
+ }
+ & .poll-option-color{
+ z-index: 19;
+ position: absolute;
+ top: 0;
+ left: 0;
+ background-color: var(--huang);
+ opacity: 0.6;
+ height: 100%;
+ }
+ }
+ & .poll-option-name{
+ max-width: 75%;
+ }
+
+ & .poll-option.open:hover{
+ border:2px solid green;
+ cursor: pointer;
+ }
+ & .poll-option.not-voted{
+ cursor: pointer;
+ }
+ & .poll-option.voted{
+ border: 2px solid blue;
+ }
+}
+@media (min-width: 800px){
+ .poll{
+ width: 50%;
+ padding: 1rem;
+ }
+
+}
+@media (max-width: 800px){
+ .poll{
+ width: 80%;
+ padding: 0.3rem;
+ }
+
+}
+#poll-button{
+ border: none;
+ background: url('https://s3.spandrell.ch/assets/board/ui/poll.svg');
+ background-size: cover;
+ width: 40px;
+ height: 40px;
+}
+.copied{
+ border: 2px solid green;
+ border-radius: 50%;
+}
+'''
+++ thread-js ^~ %- trip
+'''
+function threadJS(){
+ const buttons = document.querySelectorAll(".buttons");
+ buttons.forEach(el => {
+ const parent = el.closest(".reply");
+ if (!parent) return
+ const cb = el.querySelector(".copy-button")
+
+ cb.addEventListener("click", () => {
+ const bp = `https://spandrell.ch/board`
+ const path = document.getElementById("board-thread").getAttribute("path");
+ navigator.clipboard.writeText(bp + path).then(res => {
+ console.log(res, "write to clipboard")
+ cb.classList.add("copied");
+ setTimeout(() => cb.classList.remove("copied"), 1000)
+ }).catch(er => {
+ console.log(er, "couldn't write to clipboard")
+ })
+ })
+
+ const db = el.querySelector(".down-button")
+ db.addEventListener("click", () => {
+ parent.querySelector("footer").scrollIntoView();
+ })
+ });
+ const up = document.querySelector("#go-up")
+ up.addEventListener("click", () => {
+ document.querySelector("#top").scrollIntoView();
+ })
+ const down = document.querySelector("#go-down")
+ down.addEventListener("click", () => {
+ document.querySelector("#bottom").scrollIntoView();
+ })
+}
+
+function scanTweets(){
+ const nodes = document.querySelectorAll(".parsed-twatter");
+ for (let n of nodes){
+ const status = n.getAttribute("status");
+ twttr.widgets.createTweet(status, n.parentElement, {
+ conversation: true
+ })
+ }
+}
+window.addEventListener('load', () => {
+ scanTweets();
+ threadJS();
+})
+function composerJS(){
+ const upl = document.getElementById("upload-img");
+ upl.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ })
+
+}
+'''
+++ $ ^- manx
+:: =/ index (mul (dec page) board-page-size:constants)
+:: =/ is-op .=(thread.p.fn [author.p.fn id.p.fn])
+=/ is-op .y
+=/ author=@p ship.pid.thread
+=/ path-string (trip (spat path.thread))
+;div#board-thread.fsy
+ =path path-string
+ ;div#top;
+ ;style:"{css}"
+ ;header.fxc
+ :: ;+ ?: is-op
+ ;h1.tc:"{(trip title.thread)}"
+ :: ;h3.tc:"From {(trip title.p.fn)}"
+ ;* ?. ?|((is-admin:lib src.bowl) .=(author src.bowl)) ~
+ :~(edit-button delete-button)
+ ==
+ ;div#posts
+ ;+ inline
+ ;+ ?. is-mobile sidebar mobile-footer
+ ==
+ ;script:"{thread-js}"
+==
+++ inline
+ ;div#replies
+ :: ;* (cursor newer.children "Newer" search)
+ ;* %+ mapi:sr p.children thread-child
+ ;* (cursor bot.children "Older" search)
+ ==
+++ cursor
+|= [cursor=@ud label=tape search=(unit @t)] ^- marl
+ =/ len (lent replies.thread)
+ ?: (gte +(cursor) len) ~
+ =/ thread-path (enc:kaji pid.thread)
+ =/ cursor-path (enc:kaji cursor)
+ =/ path ?~ search
+ "/board/f/replies/{thread-path}/{cursor-path}"
+ "/board/f/replies/{thread-path}/{cursor-path}?query={(trip u.search)}"
+
+ ;+ ;div.cursor(kaji "iscroll", path path, cont "#replies", where "bottom")
+ ;button:"{label} posts"
+ ==
+
+++ edit-button
+=/ pid-string (enc:kaji pid.thread)
+;a/"/board/edit/{pid-string}"
+ ;img@"https://s3.spandrell.ch/assets/board/ui/edit.svg";
+==
+++ delete-button
+=/ pid-string (enc:kaji pid.thread)
+;a
+ =kaji "poke"
+ =action "del-thread"
+ =name "pid"
+ =payload pid-string
+ ;img@"https://s3.spandrell.ch/assets/board/ui/delete.svg";
+==
+
+++ thread-child
+|= [post-index=@ud =post:tp] ^- manx
+:: =/ base-index (mul (dec page) board-page-size:constants)
+:: =/ pind (add base-index +(post-index))
+=/ post-index (add post-index top.children)
+=/ usr ?: is-mobile
+ (user author.post contacts 60)
+ (user author.post contacts 64)
+=/ pid-string (enc:kaji [author.post id.post])
+=/ jammed-parents %- enc:kaji [op=pid.thread parent=[author.post id.post]]
+;div.reply(id pid-string)
+ ;div.author
+ ;+ avatar.usr
+ ;+ name.usr
+ ==
+ ;div.metadata
+ ;+ (date post post-index)
+ ;* ?: .=(post-index (dec (lent p.children))) ;+ ;div#bottom; ~
+ ;+ (buttons post pid-string)
+ ==
+ ;div.content
+ ;* (quote post)
+ ;* (content:pt contents.post)
+ ;* ?: .=(post-index 0) ;+ (footer post) ~
+ ==
+ ;+ (reply-box post name.usr jammed-parents)
+==
+++ quote
+|= p=post:tp ^- marl
+?~ parent.p ~
+?: .=(u.parent.p thread.p) ~
+=/ usr (user ship.u.parent.p contacts 0)
+;+ ;div.replying.flex
+ =kaji "modal"
+ =path "/board/f/snip/{(enc:kaji u.parent.p)}"
+ ;span:"Replying to:"
+ ;+ name.usr
+ ==
+
+
+++ date
+|= [=post:tp post-index=@ud]
+=/ date-string (datetime-to-tape:string:sr id.post "-")
+=/ posted ;span.posted-date:"Posted: {date-string}"
+=/ last-edit=time key:head:(pop:corm:tp contents.post)
+=/ =marl ?: .=(last-edit id.post) posted^~
+;= ;+ posted
+ ;span.updated-date:"Edited: {(datetime-to-tape:string:sr last-edit "-")}"
+==
+;div.date
+ ;p.timestamps
+ ;* marl
+ ==
+ ;p.meta
+ ;span:"Post #{(scow:parsing:sr %ud +(post-index))}/{<+((lent replies.thread))>}"
+ ;span.engagement:""
+ ==
+==
+++ buttons
+|= [=post:tp id=tape]
+:: =/ votes %+ roll ~(tap by reacts.engagement.post) |= [[s=@p [r=@t *]] acc=@]
+:: ?: ?|(=(r "thumbsup") =(r "👍"))
+;div.buttons
+ ;* ?. ?|((is-admin:lib src.bowl) .=(author.post src.bowl)) ~
+ ;+ ;img@"https://s3.spandrell.ch/assets/board/ui/edit.svg"
+ =kaji "scry"
+ =path "/board/f/edit/{id}"
+ =swap "swap"
+ =where "outer"
+ =targ "[id=\"{id}\"] .content"
+ ;
+ ==
+ ;img.copy-button@"https://s3.spandrell.ch/assets/board/ui/copy.svg";
+ ;img
+ =src "https://s3.spandrell.ch/assets/board/ui/upvote.svg"
+ ;
+ ==
+ ;img@"https://s3.spandrell.ch/assets/board/ui/downvote.svg"
+ ;
+ ==
+ ;img.down-button@"https://s3.spandrell.ch/assets/board/ui/down.svg";
+ ;img@"https://s3.spandrell.ch/assets/board/ui/reply.svg"
+ =kaji "toggle"
+ =targ ".reply-box"
+ =modal "1"
+ ;
+ ==
+ ;img.collapse-button
+ =src "https://s3.spandrell.ch/assets/board/ui/collapse.svg"
+ =kaji "toggle"
+ =targ ".content/.avatar/.uncollapse-button/.collapse-button"
+ ;
+ ==
+ ;img.uncollapse-button
+ =src "https://s3.spandrell.ch/assets/board/ui/uncollapse.svg"
+ =kaji "toggle"
+ =targ ".content/.avatar/.uncollapse-button/.collapse-button"
+ =hidden ""
+ ;
+ ==
+==
+++ reply-box
+|= [parent=post:tp author=manx jammed-parents=tape] ^- manx
+=/ parent-text=tape (content-to-md:ui contents.parent)
+=/ snip ?: (gth (lent parent-text) 150)
+ "{(scag 150 parent-text)}..."
+ parent-text
+=/ error-div "err{jammed-parents}"
+=/ subscription (subscription-type:wal now.bowl)
+:: %- serve-modal:kaji :_ ~
+;div.reply-box
+ =hidden ""
+ ;form
+ =kaji "poke"
+ =action "add-reply"
+ ;div.bordered
+ ;blockquote
+ ;div.quoting-author
+ ;span:"Replying to"
+ ;+ author
+ ==
+ ;div.content:"{snip}"
+ ==
+ ;textarea
+ =name "text"
+ =rows "10"
+ =placeholder "Your reply"
+ ;
+ ==
+ ==
+ ;input(type "hidden", name "parents", value jammed-parents);
+ ;input(type "hidden", name "title", value "");
+ ;input(type "hidden", name "error-div", value "#{error-div}");
+ ;span.error-div(id error-div);
+ ;div.reply-buttons
+ ;* ?~ subscription ~
+ ;= ;button#poll-button(type "button")
+ =kaji "scry"
+ =swap "add"
+ =cont ".bordered"
+ =path "/board/f/poll/new"
+ ;
+ ==
+ ;input#upload-img@"https://s3.spandrell.ch/assets/board/octicons/image-24.svg"(type "image");
+ ==
+ ;input(type "submit", value "Send");
+ ==
+ :: ==
+ ==
+==
+++ footer
+|= =post:tp
+=/ count ~(wyt in tags.post)
+=/ tags ?. (gth count 0) ;span;
+=/ tag-links
+%+ mapi:sr ~(tap in tags.post)
+ |= [i=@ t=@t] ^- manx =/ tt (trip t) =/ path "/board?t={tt}"
+ =/ show ?: .=(i (dec count)) tt "{tt}, "
+ ;a/"{path}":"{show}"
+;div.tags.flex
+ ; Tags:
+ ;* tag-links
+==
+;footer
+ ;+ tags
+==
+
+ ++ mobile-footer
+ =/ blog-path (trip (spat (blog-path:lib thread)))
+ ;footer
+ ;a/"{blog-path}"
+ ;button:"Blog view"
+ ==
+ ;button#go-up:"Up"
+ ;button#go-down:"Down"
+ ;button
+ =kaji "toggle"
+ =targ ".reply-box"
+ =modal "1"
+ ; Reply
+ ==
+ ==
+
+ ++ sidebar
+ :: =/ from +((mul (dec page) board-page-size:constants))
+ :: =/ to
+ :: =/ added (add from board-page-size:constants)
+ :: ?: (gth added (lent children)) (lent children) added
+ :: =/ froms (number:string:sr from)
+ :: =/ tos (number:string:sr to)
+ =/ post-count (number:string:sr (lent p.children))
+ =/ blog-path (trip (spat (blog-path:lib thread)))
+:: =/ max-page +((div (lent children) board-page-size:constants))
+ :: =/ page-count %- number:string:sr max-page
+ :: =/ prev-page %- number:string:sr ?: .=(page 1) 1 (dec page)
+ :: =/ curr-page (number:string:sr page)
+ :: =/ next-page %- number:string:sr ?: .=(page max-page) page +(page)
+ ;div#sidebar
+ :: ;div#search-box
+ :: ;form.flex
+ :: ;input(name "thread-search", placeholder "Search on Thread");
+ :: ;button.icon-button
+ :: ;img@"https://s3.spandrell.ch/assets/board/ui/search.svg"(type "image");
+ :: ==
+ :: ==
+ :: ==
+ ;a/"{blog-path}"
+ ;button:"Blog view"
+ ==
+ ;button#go-up:"Go To Top"
+ ;button#go-down:"Go To Bottom"
+ ;button
+ =kaji "toggle"
+ =targ ".reply-box"
+ =modal "1"
+ ; Reply
+ ==
+ ==
+++ snippet
+|= =post:tp ^- manx
+=/ date-string (datetime-to-tape:string:sr id.post "-")
+=/ usr (user author.post contacts 64)
+;div.snippet.reply
+ ;div.author
+ ;+ avatar.usr
+ ;+ name.usr
+ ==
+ ;div.metadata
+ ;div.date
+ ;p.timestamps
+ ;span.posted-date:"Posted: {date-string}"
+ ==
+ ==
+ ==
+ ;div.content
+ ;* (quote post)
+ ;* (content:pt contents.post)
+ :: ;+ (footer post)
+ ==
+==
+--