diff options
author | polwex <polwex@sortug.com> | 2025-06-27 22:53:52 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-06-27 22:53:52 +0700 |
commit | 328ebe85135912678bdacd3381126ffd66ef2761 (patch) | |
tree | 365962bf45302f2a440f766a4f3c9e0a962dbe47 /desk/web |
init
Diffstat (limited to 'desk/web')
79 files changed, 8743 insertions, 0 deletions
diff --git a/desk/web/about.hoon b/desk/web/about.hoon new file mode 100644 index 0000000..71783bd --- /dev/null +++ b/desk/web/about.hoon @@ -0,0 +1,65 @@ +=< about +|% +++ about +;div.blog + ;p: Welcome to Bloody Shovel 4. The latest iteration of Spandrell's blog. Now revived as a larger community app. In addition to the old blog (which will be continued), this website now provides a discussion forum (the Board), live group chats and more functionality to come. + + ;p: The blog is open to read by everyone, and there is one public chat and one public board. Everything else is login only. To login you will need an Urbit ID. They're easy to get these days. Click on the Login page for details. + + ;p: Logging in allows you to leave comments on the blog, and post on the main chat and most forum boards. Also you can watch Spandrell TV. + ;p + ; All other functionality is exclusive to Subscribers. For more details please visit + ;a/"/subscribe":"the subscribe page" + ; . + == + + ;p: The basic building blog of this website is tags. Every blogpost and board thread is tagged. You can search for tags easily in the search page and board selection page. + + ;p: If you're new here, what follows is the old text explaining what the blog is about. I'm happy to say I still stand by all my old opinions, even though of course I have new ones which I will develop further in new posts on the blog. Stay tuned. + + + ;p: I am a European man living in Asia who blogs about the past and future of civilization. + ;p: I started this blog in 2011, and since then I developed a few theories which have been influential. The background of my thought is that modern Civilization, more precisely modern Western Civilization, is on a death spiral. I try to use the insights taken from world history, the theory of evolution and pragmatic philosophy to understand why. + ;p + ;span: My self-selected bests posts can be found seen at this link: + ;a/"/blog?t=Top%20Post":"Top Posts" + == + ;p: Here are some of my core theses: + ;a/"/blog?t=WNANR" + ;h2: We Need a New Religion + == + ;p: Humans are social animals, and religions are the way that humans build social capital so that a culture can last and prosper. The present religions aren’t doing a very good job, so we should think about developing a new one. + ;a/"/2015/10/9/the-social-module" + ;h2: Status Points theory + == + ;p: Human minds do not have rational and irrational halves. Humans have evolved to care about social status, and all our behavior is designed to maximize it. Behavior which is often regarded as “irrational” should be understood as status-seeking behavior which happens to sacrifice other more immediate concerns. All people really care is about their position among their group of peers, their Dunbar circle. + ;a/"/blog?t=signaling" + ;h2: Signaling spirals + == + ;p: All religions and ideologies contain silly, even absurd ideas. These ideas impose some costs into society, but they are necessary in order to ensure the loyalty of all members of society. In good, stable times, there is a set amount of ideas one must believe, but in times of political instability, when loyalty and cohesion in the power structure is broken, new absurd ideas and practices start to appear at an increasingly fast pace, creating societal chaos and cultural destruction. + ;a/"/blog?t=men" + ;h2: Sex Wars are real and universal + == + ;p: Biology is quite simply a long history of conflict. Conflict is everywhere within and without species; entities grow different to compete better, or become more similar to prevail in conflict. Sexual species require some degree of cooperation to survive, but not a lot. The optimal mating strategy of the sexes is antagonistic, and there's no way around that. The smarter and wealtheir we get the worse the conflict will become. + ;a/"/blog?t=islam" + ;h2: Islam is Toxic Masculinity + == + ;p: Islam in its radical will continue to grow because it is good for men. Feminism lowers the incentive for men to raise families, hence the low birth rates all across the world. Islam allows men to dominate their women, which makes it easier for the average man to raise a family, hence the higher birth rates in Muslim countries. The shock that Muslim men feel on encountering Feminism in Europe is the biggest factor in driving Muslim men to Islamic terrorism. + ;a/"/2015/6/3/the-purpose-of-absurdity" + ;h2: Point deer, make horse + == + ;p: Any group, any political body requires the loyalty of its members in order to survive. It is not easy, though, to check people for their loyalty. Besides surveillance, a common method is to force people to make costly signals, i.e. make them do things that are hard for them, which lower their status. A common way of doing this is to force people to believe absurd things, which has been recorded in Chinese history more than 2,000 years ago. + ;a/"/2013/3/26/lee-kuan-yew-drains-your-brains-for-short-term-gain" + ;h2: IQ Shredders + == + ;p: High IQ people are driven to big cities, lured good education and employment opportunities. However once they get there, they are drawn to a rat-race of competition for jobs and for sexual opportunities. This, added to the complete emancipation of women, leads to the best human capital in the world delaying marriage and childbirth, which results in extremely low birth rates, in some places (e.g. Singapore) with TFR’s below 1. This results in the systematic destruction of the genetic material which produces high productivity in our modern economies. + ;a/"/blog?t=bioleninism" + ;h2: Biological Leninism (Bioleninism) + == + ;p: In order to sell a product, you make give something people want. In order to build a political movement, you must give people higher status than they presently have. The key innovation of industrial-era politics was Leninism, which built a movement with those who were socially deprived in traditional society due to their class; peasants, workers and foreigners, which were loyal to the Communist project in exchange for erasing the class and tribe distinctions of traditional society. In the post-industrial world, the mainstream political force, Progressivism, has build an effectively one-party regime by recruiting those who were lower status in both traditional and industrial society, ie. pre-1960 society due to their biological characteristics: unmarried women, homosexuals, people of different races. + ;p: Most of the theories above were developed not by myself alone, but are the result of many years through many years of debate and discussions with like-minded friends, discussions held in this blog and on other online and real venues. Some people call us reactionaries, others neoreactionaries, others #fullreactionaries. If you are more interested in these ideas you can reach me by email, interact with the community of commenters at this blog, or read other friendly sites, such as Social Matter, Thermidor, Outside in, Jim’s, or Mencius Moldbug’s. + ;p: My writing tends to have a focus on language, on using proper and accurate terms to name things, and how unclear language tends to have political intentions behind them. I also write often about international politics, which a focus on East Asia. China is growing to be a power which can rival the global hegemony of the United States. I believe that to be a good thing, but in opinion, informed by my knowledge of the people and my readings of its history, China is very different from Western powers, and has neither the interest nor the capability to run a global empire where it exports its culture and institutions. + ;p: I personally moderate the comments on the blog. I will delete Any uncivil comment, and ban reincident or hostile people. + ;p: I can be reached at site -at-spandrell●ch or on urbit as ~docteg-mothep + == +-- diff --git a/desk/web/assets/favicon.ico b/desk/web/assets/favicon.ico Binary files differnew file mode 100644 index 0000000..7bb609b --- /dev/null +++ b/desk/web/assets/favicon.ico diff --git a/desk/web/assets/fonts/arvo-bold-italic.ttf b/desk/web/assets/fonts/arvo-bold-italic.ttf Binary files differnew file mode 100644 index 0000000..b87118b --- /dev/null +++ b/desk/web/assets/fonts/arvo-bold-italic.ttf diff --git a/desk/web/assets/fonts/arvo-bold.ttf b/desk/web/assets/fonts/arvo-bold.ttf Binary files differnew file mode 100644 index 0000000..38341b1 --- /dev/null +++ b/desk/web/assets/fonts/arvo-bold.ttf diff --git a/desk/web/assets/fonts/arvo-italic.ttf b/desk/web/assets/fonts/arvo-italic.ttf Binary files differnew file mode 100644 index 0000000..1a19337 --- /dev/null +++ b/desk/web/assets/fonts/arvo-italic.ttf diff --git a/desk/web/assets/fonts/arvo-regular.ttf b/desk/web/assets/fonts/arvo-regular.ttf Binary files differnew file mode 100644 index 0000000..d8d0ec8 --- /dev/null +++ b/desk/web/assets/fonts/arvo-regular.ttf diff --git a/desk/web/assets/fonts/crimsontext-bold-italic.ttf b/desk/web/assets/fonts/crimsontext-bold-italic.ttf Binary files differnew file mode 100644 index 0000000..233e387 --- /dev/null +++ b/desk/web/assets/fonts/crimsontext-bold-italic.ttf diff --git a/desk/web/assets/fonts/crimsontext-bold.ttf b/desk/web/assets/fonts/crimsontext-bold.ttf Binary files differnew file mode 100644 index 0000000..327757c --- /dev/null +++ b/desk/web/assets/fonts/crimsontext-bold.ttf diff --git a/desk/web/assets/fonts/crimsontext-italic.ttf b/desk/web/assets/fonts/crimsontext-italic.ttf Binary files differnew file mode 100644 index 0000000..e8fe8b8 --- /dev/null +++ b/desk/web/assets/fonts/crimsontext-italic.ttf diff --git a/desk/web/assets/fonts/crimsontext-regular.ttf b/desk/web/assets/fonts/crimsontext-regular.ttf Binary files differnew file mode 100644 index 0000000..82dfeab --- /dev/null +++ b/desk/web/assets/fonts/crimsontext-regular.ttf diff --git a/desk/web/assets/fonts/crimsontext-semibold-italic.ttf b/desk/web/assets/fonts/crimsontext-semibold-italic.ttf Binary files differnew file mode 100644 index 0000000..66b129b --- /dev/null +++ b/desk/web/assets/fonts/crimsontext-semibold-italic.ttf diff --git a/desk/web/assets/fonts/crimsontext-semibold.ttf b/desk/web/assets/fonts/crimsontext-semibold.ttf Binary files differnew file mode 100644 index 0000000..19c4c4c --- /dev/null +++ b/desk/web/assets/fonts/crimsontext-semibold.ttf diff --git a/desk/web/assets/fonts/lol.ico b/desk/web/assets/fonts/lol.ico new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/assets/fonts/lol.ico diff --git a/desk/web/assets/search.svg b/desk/web/assets/search.svg new file mode 100644 index 0000000..5d48d1b --- /dev/null +++ b/desk/web/assets/search.svg @@ -0,0 +1,13 @@ +<svg version="1.1" id="_x32_" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 512 512" style="width: 256px; height: 256px; opacity: 1;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#4B4B4B;} +</style> +<g> + <path class="st0" d="M495.272,423.558c0,0-68.542-59.952-84.937-76.328c-24.063-23.938-33.69-35.466-25.195-54.931 + c37.155-75.78,24.303-169.854-38.72-232.858c-79.235-79.254-207.739-79.254-286.984,0c-79.245,79.264-79.245,207.729,0,287.003 + c62.985,62.985,157.088,75.837,232.839,38.691c19.466-8.485,31.022,1.142,54.951,25.215c16.384,16.385,76.308,84.937,76.308,84.937 + c31.089,31.071,55.009,11.95,69.368-2.39C507.232,478.547,526.362,454.638,495.272,423.558z M286.017,286.012 + c-45.9,45.871-120.288,45.871-166.169,0c-45.88-45.871-45.88-120.278,0-166.149c45.881-45.871,120.269-45.871,166.169,0 + C331.898,165.734,331.898,240.141,286.017,286.012z" style="fill: rgb(75, 75, 75);"></path> +</g> +</svg>
\ No newline at end of file diff --git a/desk/web/assets/spinner.svg b/desk/web/assets/spinner.svg new file mode 100644 index 0000000..a6763ca --- /dev/null +++ b/desk/web/assets/spinner.svg @@ -0,0 +1 @@ +<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_0XTQ{transform-origin:center;animation:spinner_y6GP .75s linear infinite}@keyframes spinner_y6GP{100%{transform:rotate(360deg)}}</style><path class="spinner_0XTQ" d="M12,23a9.63,9.63,0,0,1-8-9.5,9.51,9.51,0,0,1,6.79-9.1A1.66,1.66,0,0,0,12,2.81h0a1.67,1.67,0,0,0-1.94-1.64A11,11,0,0,0,12,23Z"/></svg>
\ No newline at end of file diff --git a/desk/web/blog/post-header.hoon b/desk/web/blog/post-header.hoon new file mode 100644 index 0000000..3bee975 --- /dev/null +++ b/desk/web/blog/post-header.hoon @@ -0,0 +1,118 @@ +/- tp=trill-post +/+ sr=sortug, lib=boke, wall +=< html +|% +++ css ^~ %- trip +''' + #post-header{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + font-size: 16px; + font-weight: 400; + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; + line-height: 1.618; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + align-items: center; + border-bottom-color: rgb(223, 223, 223); + border-bottom-style: dotted; + border-bottom-width: 1px; + box-sizing: border-box; + margin-left: 0px; + margin-right: 0px; + padding-bottom: 30px; + padding-left: 32px; + padding-right: 32px; + padding-top: 30px; + text-align: center; + + & h2{ + font-size: 2.1rem; + text-transform: uppercase; + } + + & #post-meta, .comment-meta, .entry-footer { + font-size: 90%; + font-style: italic; + color: #969696; + } + @media (min-width: 768px){ + & .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + } + & .breadcrumbs { + list-style: none; + display: inline-block; + margin: 0; + padding: 0; + border: none; + background: transparent; + text-indent: 0; + + } + & .breadcrumbs li { + display: inline-block; + margin: 0 0.3rem; + padding: 0; + border: none; + background: transparent; + text-indent: 0; + } + & .tags{ + display: block; + text-transform: capitalize; + } + + } +''' +++ html +|= [t=thread:tp who=@p] ^- manx +=/ url (trip (spat path.t)) :: TODO check for relative/absolute path +=/ date (date-to-tape:string:sr id.pid.t "-") + +=+ [[a y] m [d h mm s f]]=(yore id.pid.t) +=/ ys %- trip (ud-to-cord:string:sr y) +=/ ming (get-name:lib ship.pid.t) +=/ nam + ?. .=(ship.pid.t ~docteg-mothep) ming + "Spandrell" :: TODO should use the user component here + +=/ ttags (tags:wall ~(tap in tags.t)) +:: +;div#post-header + ;style:"{css}" + ;h2:"{(trip title.t)}" + ;div#post-meta + ;span + ; Posted by {nam} on + ;ul.breadcrumbs + ;li + ;a/"/{ys}":"{ys}" + == + ; / + ;li + ;a/"/{ys}/{<m.m>}":"{<m.m>}" + == + ; / + ;li + ;a/"/{ys}/{<m.m>}/{<d.d>}":"{<d.d>}" + == + == + == + ;+ ?: .=(0 (lent ttags)) ;span:"" + ;div.tags + ;* %+ mapi:sr ttags + |= [i=@ t=@t] ^- manx + =/ tt (trip t) + =/ path "/blog?t={tt}" + =/ show + ?. .=(i (dec (lent ttags))) "{tt}, " tt + ;a/"{path}":"{show}" + == + == +== +-- diff --git a/desk/web/blog/post-list.hoon b/desk/web/blog/post-list.hoon new file mode 100644 index 0000000..35a3590 --- /dev/null +++ b/desk/web/blog/post-list.hoon @@ -0,0 +1,159 @@ +/- tp=trill-post +/+ sr=sortug, plib=trill-utils, lib=boke, kaji +/= content /web/components/post-text +|% +++ abbreviated-post +|= [cm=content-map:tp button=manx] ^- (list manx) + =/ abbreviated (abbreviate-post:plib cm 1.000) + =/ blocks (latest-contents:plib cm) + ?: .=(blocks abbreviated) %+ turn blocks block:content + %+ snoc (turn abbreviated block:content) button + +++ post-snippet +|= t=thread:tp ^- manx + =/ url (trip (spat path.t)) :: TODO check for relative/absolute path + =+ [[a y] m [d h mm s f]]=(yore id.pid.t) + =/ ys %- trip (ud-to-cord:string:sr y) + :: =/ author (scot %p p.author.p) + =/ count (lent replies.t) + =/ comment-count ?: .=(count 0) "No" "{<count>}" + :: + =/ continue-button + ;div.continue-button-wrapper + ;a.continue-button/"{url}":"Continue" + == + =/ comment-div + ;span + ;a/"{url}#comments":"{comment-count} comments" + == + =/ tag-links + %+ mapi:sr ~(tap in tags.t) + |= [i=@ tg=@t] ^- manx + =/ tt (trip tg) + =/ path "/blog?t={tt}" + =/ show ?: .=(i (dec ~(wyt in tags.t))) tt "{tt}, " + ;a/"{path}":"{show}" + +:: +;article + ;header + ;h2 + ;a/"{url}":"{(trip title.t)}" + == + == + ;main + ;* %+ turn snip.t block:content + ;+ continue-button + == + ;div.post-metadata + ;span + ; Posted on + ;a/"{ys}":"{ys}" + ;a/"{ys}/{<m.m>}":"/{<m.m>}" + ;a/"{ys}/{<m.m>}/{<d.d>}":"/{<d.d>}" + == + :: ;span + :: ; by + :: ;a/"by/{author}":"{author}" + :: == + == + ;+ ?: .=(0 ~(wyt in tags.t)) ;footer ;+ comment-div == + ;footer + ; Tagged as + ;span.tags-links + ;* tag-links + == + ;+ comment-div + == +== +++ css ^~ %- trip +''' +#post-list{ + :: top: 0; + margin: auto; + + & article{ + margin: 0 0 1.5em; + margin-top: 40px; + border-bottom: 1px dotted #dfdfdf; + padding: 0 2rem; + padding-bottom: 40px; + + & header h2{ + font-size: 1.8rem; + text-align: center; + letter-spacing: 1px; + text-transform: uppercase; + line-height: 1.4em; + font-weight: 500; + } + & .post-metadata, article footer{ + font-size: 90%; + font-style: italic; + color: #969696; + } + + & main{ + margin: 1.5em 0 0; + } + } + & .tags-links{ + text-transform: capitalize; + margin-right: 1ch; + } + + & .continue-button-wrapper{ + margin-top: 1.5rem; + margin-bottom: 1.5rem; + } + & .continue-button{ + font-family: 'Montserrat', sans-serif; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 13px; + -webkit-transition: all .4s ease; + -o-transition: all .4s ease; + transition: all .4s ease; + padding: 0.5rem, 0rem, 0.5rem, 1.25rem; + line-height: 1.5; + border-radius: 0.2rem; + } + & .cursor{ + text-align: center; + margin: 1rem; + } +} +@media (max-width: 800px){ + #post-list{ + & article{ + padding: 0; + line-height: 1.7rem; + } + } +} + + +''' +++ test +|= [p=post:tp children=full-graph:tp] ^- manx +=/ pid [author.p id.p] +=/ pat /blog/snip/(scot %uw (jam pid)) +(fetch:kaji pat) +++ cursor +|= [cursor=(unit @da) label=tape] +?~ cursor ;div; +=/ path "/blog/f?cursor={(scow:parsing:sr %uw u.cursor)}" +;div.cursor(kaji "iscroll", path path, cont "#post-list", where "bottom") + ;button:"{label} posts" +== +++ html +|= [p=tpage:tp =marl] +^- manx +;div#post-list.blog + ;style:"{css}" + ;* marl + :: ;+ (cursor newer.p "Newer") + ;* (turn p.p post-snippet) + ;+ (cursor older.p "Older") +== +-- diff --git a/desk/web/blog/post.hoon b/desk/web/blog/post.hoon new file mode 100644 index 0000000..0366fc9 --- /dev/null +++ b/desk/web/blog/post.hoon @@ -0,0 +1,344 @@ +/- tp=trill-post +/+ plib=trill-utils, sr=sortug, kaji, lib=boke, const=constants +/= post-text /web/components/post-text +/= post-header /web/blog/post-header +|_ [[t=thread:tp =post:tp children=full-graph:tp] =bowl:gall is-mobile=?] +++ css ^~ %- trip +''' +#post-text{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + line-height: 1.618; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + border-bottom-color: rgb(223, 223, 223); + border-bottom-style: dotted; + border-bottom-width: 1px; + box-sizing: border-box; + display: block; + margin-bottom: 24px; + margin-left: 0px; + margin-right: 0px; + margin-top: 0px; + padding-bottom: 40px; + padding-left: 32px; + padding-right: 32px; + padding-top: 4px; +} +@media (max-width: 800px){ + #post-text{ + padding: 0; + line-height: 1.2; + } +} +#comments{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + line-height: 1.618; + text-align: left; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + box-sizing: border-box; + display: block; + margin-bottom: 32px; + padding-bottom: 0px; + padding-left: 16px; + padding-right: 16px; + padding-top: 0px; +} +#comments h3{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + text-align: left; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + font-size: 18.72px; + font-weight: 400; + box-sizing: border-box; + display: block; + line-height: 1.2; + margin-block-end: 9.36px; + margin-block-start: 9.36px; + margin-bottom: 9.36px; + margin-inline-end: 0px; + margin-inline-start: 0px; + margin-left: 0px; + margin-right: 0px; + margin-top: 9.36px; +} +.comment-list{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + line-height: 1.618; + text-align: left; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + box-sizing: border-box; + display: block; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + margin-block-end: 24px; + margin-block-start: 0px; + margin-bottom: 24px; + margin-inline-end: 0px; + margin-inline-start: 0px; + margin-left: 0px; + margin-right: 0px; + margin-top: 0px; + padding-inline-start: 0px; + padding-left: 0px; +} +.comment-list li{ + position: relative; + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + line-height: 1.618; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + border-left-color: rgb(223, 223, 223); + border-left-style: solid; + border-left-width: 1px; + box-sizing: border-box; + display: list-item; + padding-bottom: 16px; + padding-left: 10px; + padding-right: 0px; + padding-top: 16px; + text-align: left; +} +.comment-author{ + font-weight: 700; + color: var(--text); + font-style: normal; + font-size: 1rem; + margin-right: 1rem; +} +.comment-content{ + font-size: 90%; +} +#reply-title{ + display: inline; +} +.reply-prompt{ + color: var(--hong); + font-size: 0.9rem; +} +.nested{ + padding-left: 1rem; +} +#reply-box{ + & textarea{ + width: 100%; + } + .author-data{ + display: flex; + justify-content: space-between; + + & .name{ + margin-left: 1rem; + } + } +} +/* desktop */ +@media (min-width: 800px){ + #reply-box{ + width: 40%; + } +} +/* mobile */ +@media (max-width: 800px){ + #reply-box{ + width: 100%; + } +} +.collapse-button, .uncollapse-button{ + width: 15px; + position: absolute; + left: -10px; + top: 21px; +} +''' +++ script ^~ %- trip +''' +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(); +}) +''' +++ post-content +^- manx +;div#post-text + ;style:"{css}" + ;* (content:post-text contents.post) +== +++ comments +=/ =pid:tp [author.post id.post] +=/ board-path (trip (spat (board-path:lib t))) +=/ l (lent replies.t) +=/ length ?: .=(0 l) "No" "{<l>}" +=/ js "$store.replying_to.show('main')" +^- manx +;div#comments + ;a/"{board-path}":"Switch to Board View" + ;h3:"{length} comments" + ;div#big-reply + ;span.fl + =kaji "scry" + =path "/blog/f/reply/{(enc:kaji [pid pid])}" + =swap "add" + =cont "#big-reply" + ; Leave a reply + == + == + ;+ comment-list +== +++ comment-list ^- manx +=/ cs (tap:form:tp children) +;div#comments-wrapper + ;ul#comments-top.comment-list + =id "children{(scow %ud id.post)}" + ;* (turn (flop cs) comment) + == +== + +++ comment +=| nested=@ud +|= [=pid:tp [p=post:tp c=full-graph:tp]] +^- manx +=/ author (get-name:lib -.pid) +=/ full-time (datetime-to-tape:string:sr id.pid "/") +=/ time ?. is-mobile full-time + (post-date-ago:lib id.pid now.bowl %tam) + +=/ id=tape (enc:kaji pid) +=/ js=tape "$store.replying_to.show('{id}')" +=/ post-meta (enc:kaji [[author.post id.post] pid]) +=/ permalink "" +;li + =id "comment-{id}" + ;img.collapse-button + =src "https://s3.spandrell.ch/assets/board/ui/collapse.svg" + =kaji "toggle" + =targ ".comment-proper/.uncollapse-button/.collapse-button" + ; + == + ;img.uncollapse-button + =src "https://s3.spandrell.ch/assets/board/ui/uncollapse.svg" + =kaji "toggle" + =targ ".comment-proper/.uncollapse-button/.collapse-button" + =hidden "" + ; + == + ;div.comment + ;div.comment-meta + ;div.row + ;a.comment-author/"/u/{author}":"{author}" + ; | + ;a.comment-time + =title "{time}" + =href permalink + ; {time} + == + ;* ?. (is-admin:lib src.bowl) ~ + ;= ;span:"|" + ;a + =kaji "poke" + =action "del-comment" + =name "pid" + =payload id + ; Nuke + == + == + == + == + ;div.comment-proper + ;div.comment-content + ;* (content:post-text contents.p) + == + ;span.fl.reply-prompt + =id id + =kaji "scry" + =path "/blog/f/reply/{post-meta}" + =swap "swap" + =targ ".reply-box" + ; reply + == + ;div.reply-box; + ;+ (grandchildren [p c] +(nested)) + == + == +== +++ shit +=| nested=@ud +|= a=@ud %lol + +++ grandchildren +|= [p=full-node:tp nested=@ud] ^- manx +=/ cs (tap:form:tp children.p) +=/ c (houyi:plib children.p) +=/ count (scow:parsing:sr %ud c) + +?: ?& (gth (lent cs) 0) (gth nested 5) is-mobile == + =/ pids [[author.post id.post] [author.p.p id.p.p]] + =/ path "/blog/f/subthread/{(enc:kaji pids)}" + ;a + =kaji "scry" + =path path + =swap "swap" + =targ "#comments-wrapper" + ; Show {count} more comments + == +=/ id=tape (enc:kaji [author.p.p id.p.p]) +=/ newcomms comment :: need to tisfas it to be able to manipulate the optional variable + :: I don't get it +;ul.comment-list.nested + =id "children-{id}" + ;* (turn (flop cs) newcomms(nested nested)) +== +++ comment-subthread +|= fn=full-node:tp +=/ cs (tap:form:tp children.fn) +=/ ocs (tap:form:tp children) +=/ c (houyi:plib children.fn) +=/ oc (houyi:plib children) +=/ count (scow:parsing:sr %ud c) +=/ total (scow:parsing:sr %ud oc) +=/ =pid:tp [author.post id.post] +;div#subthread + ;div + ;p:"Showing {count} comments of {total}" + ;a + =kaji "scry" + =path "/blog/f/comments/{(enc:kaji pid)}" + =swap "swap" + =targ "#comments-wrapper" + ; Go back to top + == + == + ;div#comments-top.comment-list + =id "children{(scow %ud id.post)}" + ;* (turn (flop cs) comment) + == +== + + +++ html ^- manx + ;div.blog + ;+ (post-header t src.bowl) + ;+ post-content + ;+ comments + ;script:"{script}" + == +-- diff --git a/desk/web/blog/router.hoon b/desk/web/blog/router.hoon new file mode 100644 index 0000000..32a96c9 --- /dev/null +++ b/desk/web/blog/router.hoon @@ -0,0 +1,198 @@ +/- 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 +/= post-list /web/blog/post-list +/= post-show /web/blog/post +/= title /web/blog/title +/= user /web/components/user + +|_ [rl=req-line:kaji s=state:boke =bowl:gall] ++* fetch ~(. fetch-lib [s bowl]) + +++ eyre-bail (error-response:kaji 404) +++ manx-bail (error-page:kaji 404) +:: +++ $ ^- eyre-res:kaji + =/ p pat.rl ::?. mob.rl pat.rl [%m pat.rl] + ?+ p ~& >>> path-not-found=`path`p eyre-bail + ~ (serve-page par.rl) + [%snip pid=@ ~] (post-snippet pid.p) + :: + [%f rest=*] (fragment rest.p) + [%b rest=*] (threads rest.p) + == +++ serve-index + :- %page +:: =/ paginated (get-tag-page:plib feed.s tags.s ~['blog'] [~ ~ page-size]) + =/ paginated (thread-page-by-tags:fetch ~['blog'] [~ ~ blog-page-size:const] ~) + :: =/ paginated ~> %bout (get-tag-page:plib feed.s tags.s ~['blog'] [~ ~ page-size]) + =/ main=manx (html:post-list paginated ~) + (index ~[title main] bowl) + +++ serve-page +|= params=(map @t @t) + :- %page ^- manx + =/ tags + =/ ptags (~(get by params) 't') + ?~ ptags ~['blog'] + =/ parsed (parse-tags:ui u.ptags) + ?~ parsed ~['blog'] :: TODO some error here + ~(tap in u.parsed) + =/ older + =/ pcursor (~(get by params) 'cursor') ?~ pcursor ~ + (slaw:parsing:sr %uw u.pcursor) + =/ req [~ older blog-page-size:const] + =/ paginated (thread-page-by-tags:fetch tags req ~) + =/ tag-title + =/ ptags (~(get by params) 't') + ?~ ptags ~ + ;+ ;h3.tag-title:"Posts tagged as {(trip u.ptags)}" + =/ main=manx (html:post-list paginated tag-title) + (index ~[title main] bowl) + + + + + +++ show + :- %page + =/ dat (op-by-path:fetch pat.rl) + ?~ dat manx-bail + =/ main=manx + ~(html post-show [u.dat bowl mob.rl]) + (index ~[main] bowl) + +++ threads +|= p=(pole knot) + :- %page + =/ dat (op-by-path:fetch p) + ?~ dat manx-bail + =/ main=manx + ~(html post-show [u.dat bowl mob.rl]) + (index ~[main] bowl) +:: don't think we're using this now +++ post-snippet + |= [ss=@] + :- %html + =/ pid (dec:kaji ss pid:tp) + ?~ pid manx-bail + :: =/ post (get:gorm:tp feed.s u.pid) + =/ thread (get:torm:tp threads.s u.pid) + ?~ thread manx-bail + :: =/ fn (node-to-full:plib u.post feed.s) + (post-snippet:post-list u.thread) + + + + + + :: ?+ pat bail + :: [type=@t name=@t cursor=@t ~] + :: :: TODO %feed type at some point? + :: =/ older (slaw:parsing:sr %uw cursor.pat) ?~ older inline-error + :: ?. ?=(?(%b %t) type.pat) inline-error + :: ?: ?=(%b type.pat) + :: =/ page (board-to-page:lib name.pat tags.s feed.s [~ older board-page-size:cns]) + :: ?~ page inline-error + :: (serve-post-page name.pat u.page .n) + :: :: single tag + :: =/ page (tag-to-page:lib name.pat tags.s feed.s [~ older board-page-size:cns]) ?~ page inline-error + :: (serve-post-page name.pat u.page .y) + :: == + :: ++ serve-post-page + :: |= [name=@t =page:tp is-tag=?] + :: ~& >> serving-page=+.page + :: =/ previews ?: ?=(%blog name) + :: (turn p.page post-snippet:post-list) + :: :: + :: =/ contacts (get-contacts:cnt bowl) + :: %+ turn p.page |= fn=full-node:tp (thread-preview fn contacts bowl) + :: ;div + :: ;* %+ snoc previews + :: (cursor:cm older.page "Older" name is-tag) + :: == + ++ fragment + |= p=(pole knot) + :- %html + ?+ p manx-bail + ~ blog-page + [%comments uid=@t ~] (blog-comments uid.p) + [%subthread uid=@t ~] (subthread uid.p) + [%reply uid=@t ~] (reply-box uid.p) + == + ++ blog-page + =/ params=(map @t @t) par.rl + =/ tags + =/ ptags (~(get by params) 't') + ?~ ptags ~['blog'] + =/ parsed (parse-tags:ui u.ptags) + ?~ parsed ~['blog'] ~(tap in u.parsed) + =/ older + =/ pcursor (~(get by params) 'cursor') ?~ pcursor ~ + (slaw:parsing:sr %uw u.pcursor) + =/ req=page-req:tp [~ older blog-page-size:const] + :: =/ paginated (get-tag-page-2:plib feed.s tags.s tags req) + =/ paginated (thread-page-by-tags:fetch tags req ~) + (html:post-list paginated ~) +++ blog-comments +|= hash=@t + =/ op (op-by-hash:fetch hash) + ?~ op manx-bail + ~(comment-list post-show [u.op bowl mob.rl]) + +++ subthread +|= hash=@t + =/ both (dec:kaji hash ,[op=pid:tp sub=pid:tp]) + ?~ both manx-bail + =/ op (op-by-pid:fetch op.u.both) + ?~ op manx-bail + =/ sub-op (fn-by-pid:fetch sub.u.both) + ?~ sub-op manx-bail + =/ core ~(. post-show [u.op bowl mob.rl]) + + (comment-subthread:core u.sub-op) + +++ reply-box +|= hash=@t +?: .=(%pawn (clan:^title src.bowl)) +;div#reply-box + ;p:"You must login with an Urbit ID to comment" +== +=/ usr (user src.bowl *whoms:cnt 30) :: TODO + ;div + ;div#reply-box + ;span + =id "cancel-reply" + =class "fl" + =kaji "destroy" + =targ "#reply-box" + ; Cancel reply + == + ;form#reply-form + =kaji "poke" + =action "add-reply" + ;textarea#comment + =name "text" + =cols "45" + =rows "8" + =maxlength "6552" + =autocomplete "off" + ; + == + ;div.author-data + ;div.as.flex + ;span:"Posting as " + ;div.flex.ml1 + ;+ avatar.usr + ;+ name.usr + == + == + ;input#submit-comment(type "submit", value "Poast"); + == + ;input(type "hidden", name "parents", value (trip hash)); + ;input(type "hidden", name "is-blog", value ".y"); + ;span#error-div; + == + == + == +-- diff --git a/desk/web/blog/title.hoon b/desk/web/blog/title.hoon new file mode 100644 index 0000000..7675165 --- /dev/null +++ b/desk/web/blog/title.hoon @@ -0,0 +1,11 @@ +=< html +|% +++ html ^- manx + ;header + ;h1 + ;a/"/":"Bloody Shovel 4" + == + ;h4:"Nemo nos Salvabit" + :: ;h4:"不當而當亂也" + == +--
\ No newline at end of file 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) + == +== +-- diff --git a/desk/web/calendar/cal/day.hoon b/desk/web/calendar/cal/day.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/cal/day.hoon diff --git a/desk/web/calendar/cal/event.hoon b/desk/web/calendar/cal/event.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/cal/event.hoon diff --git a/desk/web/calendar/cal/month.hoon b/desk/web/calendar/cal/month.hoon new file mode 100644 index 0000000..1a6ec3f --- /dev/null +++ b/desk/web/calendar/cal/month.hoon @@ -0,0 +1,321 @@ +/+ sr=sortug +|_ =bowl:gall +++ css ^~ %- trip +''' +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, + segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, + sans-serif; + margin: 10vh auto; + max-width: 120ch; + background: #fffff4; + color: #140; +} + +h1 { + font-size: 1.2rem; + font-weight: bold; +} + +input, +button { + border: none; + color: inherit; + font: inherit; +} +input { + background: none; +} +input:focus-visible { + outline: solid 1px #140; +} +button { + background: #261; + color: white; + font-weight: bold; + border-radius: 0.25rem; +} +button:hover, +button:focus-visible { + background: #483; +} + +.month-control { + display: flex; +} +.month-control h2 { + font-size: 1rem; + font-weight: bold; + margin: 0; + margin-inline-end: auto; +} +.month-control a { + flex: 0 1 6ch; + text-align: end; + font-weight: bold; + color: inherit; +} + +table { + padding: 0; + width: 100%; + height: 100%; + & th{ + opacity: 0.6; + text-align: center; + padding: 0.5rem 0; + } + & td{ + border-top: solid 1px currentColor; + min-width: 0; + min-height: 8rem; + height: 4rem; + + } +} + + +.day-weekend { + color: #c88; +} +.day-past { + color: #2423; +} + +.date { + height: 100%; + display: flex; + flex-direction: column; +} +.date ul { + padding: 0; + list-style: none; +} + +.date-num { + aspect-ratio: 1; + border-radius: 100rem; + background: #fffff4; + position: absolute; + z-index: 1; +} +.day-today .date-num { + padding: 0 0.125em; + background: #220; + color: white; +} + +.create-event-form { + flex: 1 1 auto; + margin: 0; +} +.create-event { + background: none; + color: transparent; + width: 100%; + height: 100%; + cursor: crosshair; +} +.create-event:hover, +.create-event:focus-visible { + background: #efe4; +} +.create-event:focus-visible { + color: #140; +} + +.event { + margin: 0.25rem 0.5rem; +} +.event:first-child { + margin-top: 0.5rem; +} + +.event > form { + display: flex; + margin: 0; +} + +.event-button { + position: relative; + flex: 1 1 auto; + padding: 0.25rem; + background: white; + color: #000b; + border-radius: 0.25rem; + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + text-align: start; + font-size: 0.8rem; + font-weight: bold; + text-decoration: none; +} +.event-button::after { + content: ""; + position: absolute; + inset: 0; +} +.event-button:hover::after, +.event-button:focus-visible::after { + background: #fff4; +} +.day-past .event:not(:focus-within):not(:has(dialog[open])) .event-button { + opacity: 0.4; +} + +.event dialog { + inset: unset; + margin-top: 0.3rem; + padding: 0.75rem; + background: white; + border: solid 1px #ceb; + box-shadow: 0 0.25rem 0.5rem #0301, 0 0rem 2rem #0301; + z-index: 9999; +} +.event dialog::after { + content: ''; + position: absolute; + left: 0.4rem; + top: calc(-0.3rem - 2px); + width: 0.6rem; + height: 0.6rem; + transform: rotate(45deg); + background: inherit; + border: inherit; + border-right: none; + border-bottom: none; +} + +.event dialog form { + margin: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.event-dialog-row { + display: flex; + gap: 0.5rem; +} + +.event input[type="text"] { + border-bottom: solid 1px currentColor; + max-width: 15ch; +} + +.delete-button { + background: #820; +} +.delete-button:hover, +.delete-button:focus-visible { + background: #a42; +} +''' +++ html +|= time=@da ^- manx +=, chrono:userlib +=/ dat (yore time) +=+ [[a y] m [d h mm s f]]=dat +=/ mon %+ snag (dec m) mon:yu +=/ nmon %+ snag m mon:yu +=/ pmon =/ ind ?: .=(1 m) 11 (sub m 2) + %+ snag ind mon:yu +=/ year (scow:parsing:sr %ud y) +=/ nyear (scow:parsing:sr %ud +(y)) +=/ pyear (scow:parsing:sr %ud (dec y)) +=/ weekday %+ snag (daws dat) wik:yu +:: =/ weeknames (snoc +.wik:yu -.wik:yu) +=/ weeknames wik:yu +=/ first-of-month dat(d.t 1) +=/ month-starts-at (daws first-of-month) +;div#month.month + ;style: {css} + ;div.month-control + ;h2.current-month:"{mon} {year}" + ;a/"":"<- {pmon}" + ;a/"":"{nmon} ->" + == + ;table + ;tr + ;* %+ turn weeknames |= t=tape ;th.wday-name:"{t}" + == + ;* %+ turn (get-nums time) |= l=(list date) + ;tr + ;* %+ turn l cell + == + == +== ++$ row (list date) +++ get-nums +|= =time ^- (list row) +=, chrono:userlib + =/ =date (yore time) + =+ [[a y] m [d h mm s f]]=date + =/ first-of-month date(d.t 1) + =/ first-day (year first-of-month) + =/ month-start (daws first-of-month) + ~& month-start=month-start + =| l=(list row) + =| i=@ud + |- + =/ dat + ?: (gth month-start i) + =/ diff (sub month-start i) + =/ back (sub first-day (mul ~d1 diff)) + (yore back) + + =/ curr (sub i month-start) + (yore (add first-day (mul ~d1 curr))) + :: =/ nm m.added + :: =/ to-next-month (gth nm m) + :: ?: to-next-month added curr + + ?~ l $(l [~[dat] l], i +(i)) + + ?. .=(0 (mod i 7)) + =. i.l [dat i.l] + $(i +(i)) + + =. i.l (flop i.l) + =/ curr (sub i month-start) + =/ added (yore (add first-day (mul ~d1 curr))) + =/ nm m.added + =/ to-next-month (gth nm m) + ?: to-next-month (flop l) + =/ nrow ~[added] + + $(l [nrow l], i +(i)) + :: exit-condition + +++ cell +|= =date ^- manx + =/ [faded=? today=?] [.n .n] + =/ nums (scow %ud d.t.date) + ?: faded + :: + ;td.faded + ;div:"{nums}" + == + ?: today + ;td.today + ;div:"{nums}" + == + :: + ;td + ;div:"{nums}" + == + :: +++ get-num +|= [feira=@ud =time] ^- @ud +=, chrono:userlib + =/ date (yore time) + =/ first-of-month date(d.t 1) + =/ month-starts-at (daws first-of-month) + ?: (lth feira month-starts-at) + =/ diff (sub month-starts-at feira) + =/ back (sub time (mul ~d1 diff)) + d.t:(yore back) + :: e.g. month starts in day 3 (thu) and we're in day 4 (fri) + =/ diff (sub feira month-starts-at) + +(diff) +-- diff --git a/desk/web/calendar/cal/new.hoon b/desk/web/calendar/cal/new.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/cal/new.hoon diff --git a/desk/web/calendar/cal/week.hoon b/desk/web/calendar/cal/week.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/cal/week.hoon diff --git a/desk/web/calendar/cal/year.hoon b/desk/web/calendar/cal/year.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/cal/year.hoon diff --git a/desk/web/calendar/day.hoon b/desk/web/calendar/day.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/day.hoon diff --git a/desk/web/calendar/event.hoon b/desk/web/calendar/event.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/event.hoon diff --git a/desk/web/calendar/main.hoon b/desk/web/calendar/main.hoon new file mode 100644 index 0000000..bc8e5c2 --- /dev/null +++ b/desk/web/calendar/main.hoon @@ -0,0 +1,77 @@ +/- boke +|_ [s=state:boke =bowl:gall] +++ css ^~ %- trip +''' +html, body, main{ + height: 100%; + overflow-y: hidden; +} +main{ + display: flex; + & #icons{ + width: 5%; + border-right: 1px solid black; + & img{ + display:block; + margin: auto; + cursor: pointer; + width: 30px; + } + } +} +.scroll{ + height: 100%; + overflow-y: auto; +} +#main{ + display: flex; + width: 95%; +} +''' +++ $ ^- manx +;html +=data-theme "light" + ;head + ;meta(charset "utf-8"); + ;meta(name "viewport", content "width=device-width, initial-scale=1, shrink-to-fit=no"); + ;title:"Sorcal" + ;style:"{css}" + == + ;body + ;main + ;div#icons + =kaji "scry" + =swap "swap" + =targ "#main" + ;img@"https://s3.sortug.com/img/icons/notes.svg" + =path "/cal/f/notes" + ; + == + ;img@"https://s3.sortug.com/img/icons/todo.svg" + =path "/cal/f/todo" + ; + == + ;img@"https://s3.sortug.com/img/icons/calendar.svg" + =path "/cal/f/cal" + ; + == + ;select + =kaji "scry" + =path "/search/f" + =name "interval" + ;option + =value "all" + ; All time + == + ;option + =value "day" + :: =selected "" + ; Past 24h + == + == + == + ;div#main; + == + == +== +-- diff --git a/desk/web/calendar/month.hoon b/desk/web/calendar/month.hoon new file mode 100644 index 0000000..6a7d4a1 --- /dev/null +++ b/desk/web/calendar/month.hoon @@ -0,0 +1,326 @@ +/+ sr=sortug +|_ =bowl:gall +++ css ^~ %- trip +''' +body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, avenir next, avenir, + segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, + sans-serif; + margin: 10vh auto; + max-width: 120ch; + background: #fffff4; + color: #140; +} + +h1 { + font-size: 1.2rem; + font-weight: bold; +} + +input, +button { + border: none; + color: inherit; + font: inherit; +} +input { + background: none; +} +input:focus-visible { + outline: solid 1px #140; +} +button { + background: #261; + color: white; + font-weight: bold; + border-radius: 0.25rem; +} +button:hover, +button:focus-visible { + background: #483; +} + +.month-control { + display: flex; +} +.month-control h2 { + font-size: 1rem; + font-weight: bold; + margin: 0; + margin-inline-end: auto; +} +.month-control a { + flex: 0 1 6ch; + text-align: end; + font-weight: bold; + color: inherit; +} + +table { + padding: 0; + width: 100%; + height: 100%; + & th{ + opacity: 0.6; + text-align: center; + padding: 0.5rem 0; + } + & td{ + border-top: solid 1px currentColor; + min-width: 0; + min-height: 8rem; + height: 4rem; + + } +} + + +.day-weekend { + color: #c88; +} +.day-past { + color: #2423; +} + +.date { + height: 100%; + display: flex; + flex-direction: column; +} +.date ul { + padding: 0; + list-style: none; +} + +.date-num { + aspect-ratio: 1; + border-radius: 100rem; + background: #fffff4; + position: absolute; + z-index: 1; +} +.day-today .date-num { + padding: 0 0.125em; + background: #220; + color: white; +} + +.create-event-form { + flex: 1 1 auto; + margin: 0; +} +.create-event { + background: none; + color: transparent; + width: 100%; + height: 100%; + cursor: crosshair; +} +.create-event:hover, +.create-event:focus-visible { + background: #efe4; +} +.create-event:focus-visible { + color: #140; +} + +.event { + margin: 0.25rem 0.5rem; +} +.event:first-child { + margin-top: 0.5rem; +} + +.event > form { + display: flex; + margin: 0; +} + +.event-button { + position: relative; + flex: 1 1 auto; + padding: 0.25rem; + background: white; + color: #000b; + border-radius: 0.25rem; + text-wrap: nowrap; + text-overflow: ellipsis; + overflow: hidden; + text-align: start; + font-size: 0.8rem; + font-weight: bold; + text-decoration: none; +} +.event-button::after { + content: ""; + position: absolute; + inset: 0; +} +.event-button:hover::after, +.event-button:focus-visible::after { + background: #fff4; +} +.day-past .event:not(:focus-within):not(:has(dialog[open])) .event-button { + opacity: 0.4; +} + +.event dialog { + inset: unset; + margin-top: 0.3rem; + padding: 0.75rem; + background: white; + border: solid 1px #ceb; + box-shadow: 0 0.25rem 0.5rem #0301, 0 0rem 2rem #0301; + z-index: 9999; +} +.event dialog::after { + content: ''; + position: absolute; + left: 0.4rem; + top: calc(-0.3rem - 2px); + width: 0.6rem; + height: 0.6rem; + transform: rotate(45deg); + background: inherit; + border: inherit; + border-right: none; + border-bottom: none; +} + +.event dialog form { + margin: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.event-dialog-row { + display: flex; + gap: 0.5rem; +} + +.event input[type="text"] { + border-bottom: solid 1px currentColor; + max-width: 15ch; +} + +.delete-button { + background: #820; +} +.delete-button:hover, +.delete-button:focus-visible { + background: #a42; +} +''' +++ html +|= time=@da ^- manx +=, chrono:userlib +=/ dat (yore time) +=+ [[a y] m [d h mm s f]]=dat +=/ mon %+ snag (dec m) mon:yu +=/ nmon %+ snag m mon:yu +=/ pmon =/ ind ?: .=(1 m) 11 (sub m 2) + %+ snag ind mon:yu +=/ year (scow:parsing:sr %ud y) +=/ nyear (scow:parsing:sr %ud +(y)) +=/ pyear (scow:parsing:sr %ud (dec y)) +=/ weekday %+ snag (daws dat) wik:yu +:: =/ weeknames (snoc +.wik:yu -.wik:yu) +=/ weeknames wik:yu +=/ first-of-month dat(d.t 1) +=/ month-starts-at (daws first-of-month) +;html +;head; +;body +;style: {css} + ;div#month.month + ;div.month-control + ;h2.current-month:"{mon} {year}" + ;a/"":"<- {pmon}" + ;a/"":"{nmon} ->" + == + ;table + ;tr + ;* %+ turn weeknames |= t=tape ;th.wday-name:"{t}" + == + ;* %+ turn (get-nums time) |= l=(list date) + ;tr + ;* %+ turn l cell + == + == + == +== +== ++$ row (list date) +++ get-nums +|= =time ^- (list row) +=, chrono:userlib + =/ =date (yore time) + =+ [[a y] m [d h mm s f]]=date + =/ first-of-month date(d.t 1) + =/ first-day (year first-of-month) + =/ month-start (daws first-of-month) + ~& month-start=month-start + =| l=(list row) + =| i=@ud + |- + =/ dat + ?: (gth month-start i) + =/ diff (sub month-start i) + =/ back (sub first-day (mul ~d1 diff)) + (yore back) + + =/ curr (sub i month-start) + (yore (add first-day (mul ~d1 curr))) + :: =/ nm m.added + :: =/ to-next-month (gth nm m) + :: ?: to-next-month added curr + + ?~ l $(l [~[dat] l], i +(i)) + + ?. .=(0 (mod i 7)) + =. i.l [dat i.l] + $(i +(i)) + + =. i.l (flop i.l) + =/ curr (sub i month-start) + =/ added (yore (add first-day (mul ~d1 curr))) + =/ nm m.added + =/ to-next-month (gth nm m) + ?: to-next-month (flop l) + =/ nrow ~[added] + + $(l [nrow l], i +(i)) + :: exit-condition + +++ cell +|= =date ^- manx + =/ [faded=? today=?] [.n .n] + =/ nums (scow %ud d.t.date) + ?: faded + :: + ;td.faded + ;div:"{nums}" + == + ?: today + ;td.today + ;div:"{nums}" + == + :: + ;td + ;div:"{nums}" + == + :: +++ get-num +|= [feira=@ud =time] ^- @ud +=, chrono:userlib + =/ date (yore time) + =/ first-of-month date(d.t 1) + =/ month-starts-at (daws first-of-month) + ?: (lth feira month-starts-at) + =/ diff (sub month-starts-at feira) + =/ back (sub time (mul ~d1 diff)) + d.t:(yore back) + :: e.g. month starts in day 3 (thu) and we're in day 4 (fri) + =/ diff (sub feira month-starts-at) + +(diff) +-- diff --git a/desk/web/calendar/new.hoon b/desk/web/calendar/new.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/new.hoon diff --git a/desk/web/calendar/notes.hoon b/desk/web/calendar/notes.hoon new file mode 100644 index 0000000..6190e84 --- /dev/null +++ b/desk/web/calendar/notes.hoon @@ -0,0 +1,100 @@ +/- tp=trill-post +/+ ui=trill-ui +/+ kaji +|_ [notes=(list post:tp) =bowl:gall] + +++ css ^~ %- trip +''' +*{ + box-sizing: border-box; +} +#list{ + width: 20%; + border-right: 1px solid black; + & .entry{ + padding: 1rem; + cursor: pointer; + } +} +#composer{ + width: 80%; + & form{ + height: 100%; + padding: 2rem; + #inputs{ + height: 100%; + & input,textarea{ + display: block; + width: 100%; + outline: none; + } + #top{ + display: flex; + input[type=text]{ + font-size: 2rem; + font-weight: 700; + height: 2rem; + border: none; + margin-bottom: 1rem; + flex-grow: 1; + } + input[type=submit]{ + height: 2rem; + width: fit-content; + } + } + & textarea{ + height: 90%; + resize: none; + border: none; + } + } + } +} +''' +++ $ +;div + ;style: {css} + ;div#list.scroll + =kaji "scry" + =targ "#inputs" + ;* %+ turn notes note-preview + == + ;div#composer + ;form + =kaji "poke" + =action "save-note" + ;div#inputs + ;div#top + ;input(type "text", value "Title"); + ;input(type "submit", value "Save"); + == + ;textarea(name "text") + ; This is a Markdown note + == + == + == + == +== +++ note-preview +|= n=post:tp +=/ id (enc:kaji [author.n id.n]) +;div.entry + =path "/cal/f/note/{id}" + ; {(trip title.n)} +== +++ note +|= n=post:tp ^- manx +=/ pid-string (enc:kaji [author.n id.n]) +=/ post-text (content-to-md:ui contents.n) +;div + ;div#top + ;input(type "hidden", name "pid", value pid-string); + ;input(type "text", value (trip title.n)); + ;input(type "submit", value "Save"); + == + ;textarea(name "text") + ; {post-text} + == +== +-- diff --git a/desk/web/calendar/router.hoon b/desk/web/calendar/router.hoon new file mode 100644 index 0000000..793f946 --- /dev/null +++ b/desk/web/calendar/router.hoon @@ -0,0 +1,66 @@ +/- boke, tp=trill-post, cnt=contact +/+ kaji, fetch-lib=fetch, plib=trill-utils, const=constants, sr=sortug, lib=boke, ui=trill-ui +/= index /web/index +/= main /web/calendar/main +/= notes /web/calendar/notes +/= todo /web/calendar/todo +/= cal /web/calendar/cal/month + +|_ [rl=req-line:kaji s=state:boke =bowl:gall] ++* fetch ~(. fetch-lib [s bowl]) +++ eyre-bail (error-response:kaji 404) +++ manx-bail (error-page:kaji 404) +:: +++ $ ^- eyre-res:kaji + ~& >> routing-cal=rl + =/ p pat.rl ::?. mob.rl pat.rl [%m pat.rl] + ?+ p eyre-bail + ~ root + [%f rest=*] :- %html (fragment rest.p) + == +++ root + :- %page (main s bowl) + +++ fragment +|= p=(pole knot) +=/ st s + ?+ p manx-bail + [%notes ~] (notes note-list bowl) + [%todo ~] (todo st bowl) + [%cal ~] (~(html cal bowl) now.bowl) + :: + [%note uid=@t ~] (note uid.p) + [%todo uid=@t ~] manx-bail + [%cal %day uid=@t ~] manx-bail + [%cal %week uid=@t ~] manx-bail + [%cal %month uid=@t ~] manx-bail + [%cal %event uid=@t ~] manx-bail + == +++ note |= uid=@t ^- manx + =/ upid (dec:kaji uid pid:tp) + ?~ upid manx-bail + =/ =post:tp (make-note 'rofl') + (note:notes post) + +++ note-list ^- (list post:tp) + %+ turn ~['Note 1' 'Lol' 'Hoon' 'Blog ideas' 'lmao'] + make-note +++ make-note + |= t=@t ^- post:tp + =/ tokens (tokenize:ui t) + =/ tags (silt ~[t 'note']) + =/ p (build-post:lib tokens tags [our.bowl now.bowl]) + p(title t) ++$ task + $: title=@t + desc=paragraph:tp + priority=@ud + status=$?(%todo %wip %done) + created=@da + due=((mop @da @da) gth) + creator=@p + subtasks=$~(~ (list task)) + workers=(map @p @t) :: who's doing what + tags=(set @t) + == +-- diff --git a/desk/web/calendar/todo.hoon b/desk/web/calendar/todo.hoon new file mode 100644 index 0000000..311c59e --- /dev/null +++ b/desk/web/calendar/todo.hoon @@ -0,0 +1,6 @@ +|_ [s=* =bowl:gall] +++ $ +;div#todo + ;p: hi +== +-- diff --git a/desk/web/calendar/week.hoon b/desk/web/calendar/week.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/week.hoon diff --git a/desk/web/calendar/year.hoon b/desk/web/calendar/year.hoon new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/desk/web/calendar/year.hoon diff --git a/desk/web/chat/chat.hoon b/desk/web/chat/chat.hoon new file mode 100644 index 0000000..92c451a --- /dev/null +++ b/desk/web/chat/chat.hoon @@ -0,0 +1,317 @@ +/- c=tlon-channels, contact +/+ sr=sortug, kaji, sigil=sigil-sigil, lib=boke +/= user /web/components/user +|_ [=whoms:contact =bowl:gall] +:: ++$ mane $@(@tas [@tas @tas]) :: XML name+space ++$ manx $~([[%$ ~] ~] [g=marx c=marl]) :: dynamic XML node ++$ marl (list manx) :: XML node list ++$ mars [t=[n=%$ a=[i=[n=%$ v=tape] t=~]] c=~] :: XML cdata ++$ mart (list [n=mane v=tape]) :: XML attributes ++$ marx $~([%$ ~] [n=mane a=mart]) :: dynamic XML tag +++ md +''' +# H1 + +## H2 + +### H3 + +#### H4 + +##### H5 + +###### H6 + +This is a paragraph with _italics_, *bold* and +`inline code`. Sentences can be hard wrapped. + + +- unordered +- list + ++ ordered ++ list + +[link](https://urbit.org) + + + +``` +fenced codeblock +(note language spec not supported) +``` + +horizontal rule: +--- + +> block quotes + may be hard-wrapped if indented + +Backslash at end\ +of line adds linebreak + +Udon syntax may be prefixed with \*backslashes\* to escape. + +Hoon atom literals like ~sampel-palnet and ~.foo will +be rendered as inline code. + +''' +++ css ^~ %- trip +''' +#chat-box{ + height: 100%; + width: 100%; + border: 1px solid var(--text-color); + display: flex; + flex-direction: column; + + input[type=text]{ + outline: none; + } + + header{ + border-bottom: 1px solid var(--text-color); + display: flex; + padding: 0.5rem; + + h3{ + margin: 0; + } + } +} +#chat-container{ + flex-grow: 1; + overflow-y: scroll; + word-break: break-all; + padding: 0.5rem; + + + .chat-msg { + display: flex; + min-height: 4rem; + + .left { + margin-right: 1rem; + + & .avatar{ + width: 32px; + height: 32px; + } + } + .right { + width: 100%; + .metadata { + display: flex; + color: rgb(100, 100, 100); + font-size: 0.8rem; + + .name{ + font-family: "Anonymous Pro"; + margin-right: 0.5rem; + } + } + .chat-content{ + font-family: Inter; + } + } + } +} +#chat-container .chat-content img{ + max-width: 80%; + display: block; + margin: 0 auto; +} +#chat-composer{ + display: flex; +} +#text-input{ + flex-grow: 1; +} +.row{ + display: flex; +} +.f1{ + display: flex; + justify-content: space-between; +} + +@media (min-width: 800px){ + header{ + padding: 0.5rem 2rem; + + } + +} +@media (max-width: 800px){ + + header{ + & .chat-name{ + text-align: end; + } + } + +} + +''' +++ script +|= sub-path=tape +^~ %+ weld %- trip +''' +document.addEventListener('DOMContentLoaded', (event) => { + const room = document.getElementById("chat-container"); + if (room) room.scrollTop = room.scrollHeight +}) + +document.addEventListener('kaji-fact', (event) => { + const room = document.getElementById("chat-container"); + if (room) setTimeout(() => room.scrollTop = room.scrollHeight, 50) +}) + +''' +""" +subscribe('{sub-path}'); +""" +++ main +|= page=paged-posts:c +=/ sip our.bowl +=/ name 'chat' +=/ sub-path "/chat/{(scow %p sip)}/{(trip name)}" +^- manx + =/ chat-list (tap:on-posts:c posts.page) + ;div#chat-box + ;style:"{css}" + ;script:"{(script sub-path)}" + ;header.f1 + ;h3: Bloody Shovel Public Chat + ;div.chat-name:"~tasdev-docteg-mothep/bloody-shovel-public/chat" + == + ;div#chat-container + ;* %+ turn chat-list |= [=time up=(unit post:c)] + ?~ up ;span; + (post-div +<.u.up) + == + ;form + =id "chat-composer" + =kaji "poke" + =action "add-chat" + =wipe "yes" + ;input#text-input(type "text", name "input", autocomplete "off"); + ;input(type "submit", value "Submit"); + == + == +++ post-div +|= =memo:c ^- manx +:: =/ =memo:c +<.post +:: seal is engagement, essay is the post +=/ usr (user author.memo whoms 32) +;div.chat-msg + ;div.left + ;+ avatar.usr + == + ;div.right + ;div.metadata + ;+ name.usr + ;div.time:"{(time-to-tape:string:sr sent.memo)}" + == + ;div.chat-content + ;* %+ turn content.memo |= =verse:c + ?: ?=(%block -.verse) (block +.verse) + ;div.inline + ;* %+ turn p.verse tinline + == + == + == +== +:: chat:tlon -> manx +++ block |= b=block:c + ?- -.b + %image + ;img + =src "{(trip src.b)}" + :: =width "{(scow %ud width.b)}" + :: =height "{(scow %ud height.b)}" + =alt "{(trip alt.b)}" + ; + == + %cite ;p:"oops" + %header (heading-to-manx +.b) + %listing (listing-to-manx +.b) + %code ;code + =lang "{(trip code.b)}" + ; (trip lang.b) + == + %rule ;p:"" + == +:: :: +++ listing-to-manx + |= l=listing:c ^- manx + ?: ?=(%item -.l) + ;li + ;* %+ turn p.l tinline + == + :: + :: =/ inl (tinline r.l) :: ??? + ?: ?=(%ordered p.l) + ;ol + ;* %+ turn q.l listing-to-manx + == + ?: ?=(%unordered p.l) + ;ul + ;* %+ turn q.l listing-to-manx + == + :: %tasklist + ;div.task-list + ;* %+ turn q.l listing-to-manx + == +++ heading-to-manx +|= [p=?(%h1 %h2 %h3 %h4 %h5 %h6) q=(list inline:c)] ^- manx + =/ tp (turn q tinline) + ?- p + %h1 ;h1 + ;* tp + == + %h2 ;h2 + ;* tp + == + %h3 ;h3 + ;* tp + == + %h4 ;h4 + ;* tp + == + %h5 ;h5 + ;* tp + == + %h6 ;h6 + ;* tp + == + == +:: :: +++ tinline |= i=inline:c + ?@ i ;span:"{(trip i)}" + ?- -.i + %italics ;i + ;* (turn p.i tinline) + == + %bold ;em:"" + %strike ;span.strike:"" + %blockquote ;blockquote + ;* (turn p.i tinline) + == + :: + %inline-code ;code:"{(trip p.i)}" + %code ;code:"{(trip p.i)}" + %ship ;span.ship:"{(trip (scot %p p.i))}" + %block ;span.ref:"Reference to {(scow %ud p.i)} {(trip q.i)}" + %tag ;span.tag:"{(trip p.i)}" + %link ;a/"{(trip p.i)}"(target "_blank"):"{(trip q.i)}" + %task ;span.task + ; Done: {(bool-string p.i)} + ;* (turn q.i tinline) + == + %break ;br; + == +++ bool-string + |= a=? ^- tape ?:(a "yes" "no") +-- diff --git a/desk/web/components/chat.hoon b/desk/web/components/chat.hoon new file mode 100644 index 0000000..f616a16 --- /dev/null +++ b/desk/web/components/chat.hoon @@ -0,0 +1,127 @@ +/- c=tlon-channels +/+ sr=sortug, kaji, sigil=sigil-sigil, lib=boke +=< html +|% +++ css ^~ %- trip +''' +#chat-box{ + height: 100%; + width: 100%; + border: 1px solid var(--text-color); + display: flex; + flex-direction: column; + + input[type=text]{ + outline: none; + } + + header{ + border-bottom: 1px solid var(--text-color); + display: flex; + padding: 0.5rem 2rem; + + h3{ + margin: 0; + } + } +} +#chat-container{ + flex-grow: 1; + overflow-y: scroll; + word-break: break-all; + padding: 0.5rem; + + + .chat-msg { + display: flex; + min-height: 4rem; + + .left { + margin-right: 1rem; + + & .anon-avatar{ + width: 32px; + height: 32px; + } + } + .right { + width: 100%; + .metadata { + display: flex; + color: rgb(100, 100, 100); + font-size: 0.8rem; + + .author{ + font-family: "Anonymous Pro"; + margin-right: 0.5rem; + } + } + .chat-content{ + font-family: Inter; + } + } + } +} +#chat-container .chat-content img{ + max-width: 80%; + display: block; + margin: 0 auto; +} +#chat-composer{ + display: flex; +} +#text-input{ + flex-grow: 1; +} +.row{ + display: flex; +} +.f1{ + display: flex; + justify-content: space-between; +} +''' +++ script ^~ %- trip +''' +document.addEventListener('DOMContentLoaded', (event) => { + const room = document.getElementById("chat-container"); + if (room) room.scrollTop = room.scrollHeight +}) + +document.addEventListener('kaji-fact', (event) => { + const room = document.getElementById("chat-container"); + if (room) setTimeout(() => room.scrollTop = room.scrollHeight, 50) +}) +''' +++ post-to-manx +|= post=@t +;div.chat-msg + ;p:"(trip post)" +== +++ html + |= =bowl:gall ^- manx + =/ pat /chat/(scot %p our.bowl)/chat/posts/newest/200/post + =/ io ~(. io:sr bowl) + =/ scrying (scry:io %channels pat paged-posts:c) + =/ chat-list (tap:on-posts:c posts.scrying) + ;div#chat-box + ;style:"{css}" + ;script:"{script}" + ;header.f1 + ;h3: Bloody Shovel Public Chat + ;div: ~hostyr-docteg-mothep/spandrell + == + ;div#chat-container + ;* %+ turn chat-list |= [=time up=(unit post:c)] + ?~ up ;p:"Deleted post" + (tlon-chat-post-to-manx:lib u.up) + == + ;form + =id "chat-composer" + =kaji "poke" + =action "add-chat" + ;input#text-input(type "text", name "input", autocomplete "off"); + ;input(type "submit", value "Submit"); + == + == +-- diff --git a/desk/web/components/common.hoon b/desk/web/components/common.hoon new file mode 100644 index 0000000..553103a --- /dev/null +++ b/desk/web/components/common.hoon @@ -0,0 +1,11 @@ +/+ sr=sortug +|% +++ cursor +|= [cursor=(unit @da) label=tape name=@t tag=?] ^- manx +?~ cursor ;div; +=/ type ?: tag "t" "b" +=/ path "/page/{type}/{(trip name)}/{(scow:parsing:sr %uw u.cursor)}" +;div.cursor(kaji "iscroll", path path, cont "#post-list", where "bottom") + ;button:"{label} posts" +== +-- diff --git a/desk/web/components/date.hoon b/desk/web/components/date.hoon new file mode 100644 index 0000000..1526af0 --- /dev/null +++ b/desk/web/components/date.hoon @@ -0,0 +1,18 @@ +/+ sr=sortug +|_ [date=@da link=?] +++ $ +^- marl +=+ [[a y] m [d h mm s f]]=(yore date) +=/ ys %- trip (ud-to-cord:string:sr y) +?: link +;= + ;a/"{ys}":"{ys}" + ;a/"{ys}/{<m.m>}":"/{<m.m>}" + ;a/"{ys}/{<m.m>}/{<d.d>}":"/{<d.d>}" +== +;= + ;span:"{ys}" + ;span:"/{<m.m>}" + ;span:"/{<d.d>}" +== +--
\ No newline at end of file diff --git a/desk/web/components/modals.hoon b/desk/web/components/modals.hoon new file mode 100644 index 0000000..7068a97 --- /dev/null +++ b/desk/web/components/modals.hoon @@ -0,0 +1,20 @@ +/- boke +/+ kaji +|_ [s=state:boke =bowl:gall pat=path] +++ $ +?+ pat (error-page:kaji 404) +[%login ~] login +== +++ login +;div#modal-body + ;h3: Login with an Urbit ship + ;form + =kaji "navi" + ;fieldset + ;label: Urbit ID + ;input(type "text", name "ship"); + == + ;input(type "submit", value "Login"); + == +== +--
\ No newline at end of file diff --git a/desk/web/components/poll.hoon b/desk/web/components/poll.hoon new file mode 100644 index 0000000..3184ec0 --- /dev/null +++ b/desk/web/components/poll.hoon @@ -0,0 +1,208 @@ +/- *polls +/+ sr=sortug, kaji, lib=boke +|% +++ css ^~ %- trip +''' +.new-poll{ + border: 1px solid black; + width: 80%; + margin: auto; + margin-bottom: 1rem; + padding: 1rem; + #poll-title{ + font-size: 1.5rem; + padding: 0.3rem; + margin-bottom: 0.5rem; + text-align: center; + } + & input[type=text]{ + display: block; + line-height: 1.5rem; + width: 80%; + margin: 0.3rem auto; + } + #final-row{ + border-top: 1px solid black; + } + #bet-input{ + width: 30%; + display: inline-block; + margin-left: 1ch; + } +} +@media (max-width: 800px){ + #final-row{ + display: block; + } +} + +''' +++ script ^~ %- trip +''' +function newOpts(){ + const button = document.getElementById("add-opt-button"); + const container = document.getElementById("options"); + + button.addEventListener("click", (e) => { + console.log(e, "clck") + e.preventDefault(); + e.stopPropagation(); + + const currentOpts = container.querySelectorAll(".opt"); + const index = currentOpts.length; + + const input = document.createElement("input"); + input.classList.add("opt"); + input.type = "text"; + input.name = `poll-opt-${index}`; + input.placeholder = "New Option"; + input.autocomplete = "off"; + container.appendChild(input); + }) +} +setTimeout(newOpts, 500); +''' +++ form +|= =bowl:gall ^- manx +;form +;div.new-poll + ;style:"{css}" + ;input#poll-title(type "text", name "poll-title", placeholder "Poll Question"); + ;div#options + ;input.opt(type "text", name "poll-opt-0", placeholder "First Option", autocomplete "off"); + ;input.opt(type "text", name "poll-opt-1", placeholder "Second Option", autocomplete "off"); + == + ;div.f1 + ;div + ;label: Multiple Choice + ;input(type "checkbox", name "poll-multiple"); + == + ;button#add-opt-button(type "button"):"Add" + == + ;div#final-row.f1 + ;div + ;label: Require betting + ;input#bet-input(type "text", name "poll-min-bet"); + == + ;div + ;label: Expires + ;input(type "datetime-local", name "poll-expiry"); + == + == + ;script:"{script}" +== +== ++$ opt + $: text=@t + votes=(set @p) + == +++ show +|= [=poll =bowl:gall] ^- manx + =/ poll-type=tape ?: ?=(%exc -.votes.poll) "Single choice poll" "Multiple choice poll" + =/ pid-string (enc:kaji [author.poll time.poll]) + =/ total=@ud + ?: ?=(%exc -.votes.poll) + ~(wyt by p.votes.poll) + %- ~(rep by p.votes.poll) |= [it=[@ud m=(map @p (set @ud))] acc=@ud] + (add acc ~(wyt by m.it)) + + |^ + ?: (gth now.bowl expiry.poll) expired-poll fresh-poll + ++ fresh-poll ^- manx + =/ expiry-string (post-date-ago:lib now.bowl expiry.poll %yau) + ;div.poll + ;div.top-row + ;h6:"Open Poll" + ;h3.hs:"{(trip text.poll)}" + == + ;div.options + ;* %+ mapi:sr options.poll fresh-opt + == + ;div.bottom-row.f1 + ;div.poll-type:"{poll-type}" + ;div.expiry:"Expires in {expiry-string}" + == + == + ++ fresh-opt + |= [i=@ud text=@t] + =/ payload (enc:kaji [author.poll time.poll i]) + =/ stats (opt-stats i) + =/ pct (scow %ud pct.stats) + =/ style "width: {pct}%;" + =/ classes + ?: ?=(%exc -.votes.poll) + =/ have-voted (~(get by p.votes.poll) src.bowl) + ?~ have-voted + "poll-option open" + ?: .=(u.have-voted i) + "poll-option voted" + "poll-option not-voted" + :: inc + =/ opt-votes (~(get by p.votes.poll) i) + ?~ opt-votes "poll-option open" + =/ my-votes (~(get by u.opt-votes) src.bowl) + ?~ my-votes "poll-option open" + ?: (~(has in u.my-votes) i) + "poll-option voted" + "poll-option open" + + + ;div + =class classes + =kaji "poke" + =action "vote-poll" + =name "option" + =payload payload + + ;div.poll-option-color(style style); + ;div.f1.inner + =name "option-{(scow %ud i)}" + =payload "vote" + ;div.poll-option-name.hs:"{(trip text)}" + ;div:"{pct}%" + == + == + ++ expired-poll ^- manx + =/ expiry-string (post-date-ago:lib expiry.poll now.bowl %yau) + ;div.poll + ;div.top-row + ;h6:"Closed Poll" + ;h3.hs:"{(trip text.poll)}" + == + ;div.options + ;* %+ mapi:sr options.poll dead-opt + == + ;div.bottom-row + ;div.poll-type:"{poll-type}" + ;div.expiry:"Expired {expiry-string} ago" + == + == + ++ dead-opt + |= [i=@ud text=@t] ^- manx + =/ stats (opt-stats i) + =/ pct (scow %ud pct.stats) + =/ style "width: {pct}%;" + ;div.poll-option + ;div.poll-option-color(style style); + ;div.inner.f1 + ;div.poll-option-name.hs:"{(trip text)}" + ;div:"{pct}%" + == + == + ++ opt-stats + |= i=@ud ^- [count=@ud pct=@ud voters=(set @p)] + =/ voters + ?: ?=(%exc -.votes.poll) + =/ all-votes ~(tap by p.votes.poll) + %+ roll all-votes |= [[sip=@p vote=@ud] acc=(set @p)] + ?. .=(vote i) acc %- ~(put in acc) sip + :: + =/ all-votes (~(get by p.votes.poll) i) + ?~ all-votes ~ + ~(key by u.all-votes) + =/ voter-count ~(wyt in voters) + =/ pct ?: .=(total 0) 0 (div (mul voter-count 100) total) + [voter-count pct voters] + + -- +-- diff --git a/desk/web/components/post-header.hoon b/desk/web/components/post-header.hoon new file mode 100644 index 0000000..a9cb1e2 --- /dev/null +++ b/desk/web/components/post-header.hoon @@ -0,0 +1,111 @@ +/- tp=trill-post +/+ sr=sortug, lib=boke +=< html +|% +++ css ^~ %- trip +''' + #post-header{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + font-size: 16px; + font-weight: 400; + text-rendering: optimizelegibility; + -webkit-font-smoothing: antialiased; + line-height: 1.618; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + align-items: center; + border-bottom-color: rgb(223, 223, 223); + border-bottom-style: dotted; + border-bottom-width: 1px; + box-sizing: border-box; + margin-left: 0px; + margin-right: 0px; + padding-bottom: 30px; + padding-left: 32px; + padding-right: 32px; + padding-top: 30px; + text-align: center; + + & h2{ + font-size: 2.1rem; + text-transform: uppercase; + } + + & #post-meta, .comment-meta, .entry-footer { + font-size: 90%; + font-style: italic; + color: #969696; + } + @media (min-width: 768px){ + & .col-md-4 { + -ms-flex: 0 0 33.333333%; + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + } + & .breadcrumbs { + list-style: none; + display: inline-block; + margin: 0; + padding: 0; + border: none; + background: transparent; + text-indent: 0; + + } + & .breadcrumbs li { + display: inline-block; + margin: 0 0.3rem; + padding: 0; + border: none; + background: transparent; + text-indent: 0; + } + & .tags{ + display: block; + text-transform: capitalize; + } + + } +''' +++ html +|= p=post:tp ^- manx +=/ url (trip (spat (title-to-path:lib title.p id.p))) :: TODO check for relative/absolute path +=/ date (date-to-tape:string:sr id.p "-") + +=+ [[a y] m [d h mm s f]]=(yore id.p) +=/ ys %- trip (ud-to-cord:string:sr y) + +:: +;div#post-header + ;style:"{css}" + ;h2:"{(trip title.p)}" + ;div#post-meta + ;span + ; Posted by Spandrell on + ;ul.breadcrumbs + ;li + ;a/"{ys}":"{ys}" + == + ; / + ;li + ;a/"{ys}/{<m.m>}":"{<m.m>}" + == + ; / + ;li + ;a/"{ys}/{<m.m>}/{<d.d>}":"{<d.d>}" + == + == + == + ;+ ?: .=(0 ~(wyt in tags.p)) ;span:"" + ;div.tags + ;* %+ mapi:sr ~(tap in tags.p) + |= [i=@ t=@t] ^- manx + ?. .=(i (dec ~(wyt in tags.p))) + ;a/"/tags/{(trip t)}":"{(trip t)}, " + ;a/"/tags/{(trip t)}":"{(trip t)}" + == + == +== +--
\ No newline at end of file diff --git a/desk/web/components/post-list.hoon b/desk/web/components/post-list.hoon new file mode 100644 index 0000000..427aa72 --- /dev/null +++ b/desk/web/components/post-list.hoon @@ -0,0 +1,148 @@ +/- tp=trill-post +/+ sr=sortug, plib=trill-utils, lib=boke, kaji +/= content /web/components/post-text +|% +++ abbreviated-post +|= [cm=content-map:tp button=manx] ^- (list manx) +=/ abbreviated (abbreviate-post:plib cm 1.000) +=/ blocks (latest-contents:plib cm) +?: .=(blocks abbreviated) %+ turn blocks block:content +%+ snoc (turn abbreviated block:content) button + +++ post-snippet +|= [p=post:tp children=full-graph:tp] ^- manx +=/ url (trip (spat (title-to-path:lib title.p id.p))) :: TODO check for relative/absolute path +=+ [[a y] m [d h mm s f]]=(yore id.p) +=/ ys %- trip (ud-to-cord:string:sr y) +:: =/ author (scot %p p.author.p) +=/ count (houyi:plib children) +=/ comment-count ?: .=(count 0) "No" "{<count>}" +:: +=/ continue-button +;div.continue-button-wrapper + ;a.continue-button/"{url}":"Continue" +== +=/ comment-div + ;span + ;a/"{url}#comments":"{comment-count} comments" + == +=/ tag-links +%+ mapi:sr ~(tap in tags.p) + |= [i=@ t=@t] ^- manx + ?. .=(i (dec ~(wyt in tags.p))) + ;a/"/tags/{(trip t)}":"{(trip t)}, " + ;a/"/tags/{(trip t)}":"{(trip t)} " + +:: +;article + ;header + ;h2 + ;a/"{url}":"{(trip title.p)}" + == + == + ;main + ;* (abbreviated-post contents.p continue-button) + == + ;div.post-metadata + ;span + ; Posted on + ;a/"{ys}":"{ys}" + ;a/"{ys}/{<m.m>}":"/{<m.m>}" + ;a/"{ys}/{<m.m>}/{<d.d>}":"/{<d.d>}" + == + :: ;span + :: ; by + :: ;a/"by/{author}":"{author}" + :: == + == + ;+ ?: .=(0 ~(wyt in tags.p)) ;footer ;+ comment-div == + ;footer + ; Tagged as + ;span.tags-links + ;* tag-links + == + ;+ comment-div + == +== +++ css ^~ %- trip +''' +#post-list{ + :: top: 0; + margin: auto; + + & article{ + margin: 0 0 1.5em; + margin-top: 40px; + border-bottom: 1px dotted #dfdfdf; + padding: 0 2rem; + padding-bottom: 40px; + + & header h2{ + font-size: 1.8rem; + text-align: center; + letter-spacing: 1px; + text-transform: uppercase; + line-height: 1.4em; + font-weight: 500; + } + & .post-metadata, article footer{ + font-size: 90%; + font-style: italic; + color: #969696; + } + + & main{ + margin: 1.5em 0 0; + } + } + & .tags-links{ + text-transform: capitalize; + margin-right: 1ch; + } + + & .continue-button-wrapper{ + margin-top: 1.5rem; + margin-bottom: 1.5rem; + } + & .continue-button{ + font-family: 'Montserrat', sans-serif; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 13px; + -webkit-transition: all .4s ease; + -o-transition: all .4s ease; + transition: all .4s ease; + padding: 0.5rem, 0rem, 0.5rem, 1.25rem; + line-height: 1.5; + border-radius: 0.2rem; + } + & .cursor{ + text-align: center; + margin: 1rem; + } +} + + +''' +++ test +|= [p=post:tp children=full-graph:tp] ^- manx +=/ pid [author.p id.p] +=/ pat /blog/snip/(scot %uw (jam pid)) +(fetch:kaji pat) +++ cursor +|= [cursor=(unit @da) label=tape] +?~ cursor ;div; +=/ path "/page/t/blog/{(scow:parsing:sr %uw `@uw`u.cursor)}" +;div.cursor(kaji "iscroll", path path, cont "#post-list", where "bottom") + ;button:"{label} posts" +== +++ html +|= p=page:tp +^- manx +;div#post-list + ;style:"{css}" + ;+ (cursor newer.p "Newer") + ;* (turn p.p post-snippet) + ;+ (cursor older.p "Older") +== +-- diff --git a/desk/web/components/post-text.hoon b/desk/web/components/post-text.hoon new file mode 100644 index 0000000..0335e4e --- /dev/null +++ b/desk/web/components/post-text.hoon @@ -0,0 +1,182 @@ +/- b=boke, tp=trill-post +/+ sr=sortug, plib=trill-utils +/= poll-div /web/components/poll +|_ [s=state:b =bowl:gall] +++ content +|= c=content-map:tp ^- (list manx) +=/ latest (pry:corm:tp c) +?~ latest ~ +(content-list +.u.latest) +++ content-list +|= c=content-list:tp ^- (list manx) +(turn c block) +++ block +|= b=block:tp ^- manx +?+ -.b ;p; +%paragraph (pg +.b) +%blockquote (bq +.b) +%heading (heading +.b) +:: %table (table +.b) +%list (htmlist +.b) +%media (media +.b) +%codeblock (codeblock +.b) +%eval (eval +.b) +%ref (refblock +.b) +== +++ eval +|= txt=@t ^- manx +:: Ream can crash if the cord is wrong +:: soften instead +=/ uhoon (rush txt vest) +?~ uhoon ;p:"The hoon you tried to run ({(trip txt)}) is invalid." +=/ run %- mule |. +%+ slap !>(..zuse) u.uhoon +?: ?=(%.n -.run) :: if virtualization fails get a (list tank) +;p + ;span:"Evaluation of {(trip txt)} failed:" + ;br; + ;* %+ turn p.run |= t=tank ;span:"{~(ram re t)}" +== +;p:"{(text p.run)}" +++ pg +|= l=(list inline:tp) ^- manx +;p +;* %+ turn l inline +== +++ bq +|= l=(list inline:tp) ^- manx +;blockquote +;* %+ turn l inline +== +:: ++ table +:: |= rows=(list (list content-list:tp)) +:: ;table +:: ;* %+ turn rows +:: |= l=(list content-list:tp) +:: ;tr +:: ;* %+ turn l +:: |= cl=content-list:tp +:: ;td +:: ;* (turn cl block) +:: == +:: == +:: == +++ refblock +|= [app=term src=@p pat=path] + ?+ app ;span; + %polls (poll-loader pat) + == +++ poll-loader +|= scry-path=(pole knot) ^- manx +?. ?=([ship=@t id=@t ~] scry-path) ;span; +=/ usip (slaw %p ship.scry-path) +?~ usip ;span; +=/ uid (slaw %da id.scry-path) +?~ uid ;span; +=/ upoll (~(get by polls.s) [u.usip u.uid]) +?~ upoll ;span:"Poll not found" +(show:poll-div u.upoll bowl) + + +++ htmlist +|= [l=(list content-list:tp) ordered=?] +?: ordered +;ol + ;* %+ turn l li +== +;ul + ;* %+ turn l li +== +++ li +|= l=content-list:tp ^- manx +;li + ;* (turn l block) +== +++ media +|= m=media:tp ^- manx +?- -.m +%video ;video@"{(trip p.m)}"; +%audio ;audio@"{(trip p.m)}"; +%images ;div.images + ;* %+ turn p.m + |= [url=@t caption=@t] ;img@"{(trip url)}"(alt (trip caption)); + == +== +++ codeblock +|= [code=@t lang=@t] :: TODO lang suff +;pre +;code:"{(trip code)}" +== +++ heading +|= [pp=@t q=@] ^- manx +=/ p (trip pp) +?: .=(1 q) ;h1:"{p}" +?: .=(2 q) ;h2:"{p}" +?: .=(3 q) ;h3:"{p}" +?: .=(4 q) ;h4:"{p}" +?: .=(5 q) ;h5:"{p}" +?: .=(6 q) ;h6:"{p}" +;p:"" +++ inline +|= l=inline:tp ^- manx +?- -.l + %text (parse-text p.l) + %italic ;i:"{(trip p.l)}" + %bold ;strong:"{(trip p.l)}" + %strike ;del:"{(trip p.l)}" + %ship ;span.ship:"{(trip (scot %p p.l))}" + %codespan ;code:"{(trip p.l)}" + %link ?. (is-image:web:sr href.l) + ;a/"{(trip href.l)}"(target "_blank"):"{(trip show.l)}" + ;a/"{(trip href.l)}" + =target "_blank" + ;img@"{(trip href.l)}"(alt (trip show.l)); + == + %date + =/ s (time:enjs:format p.l) + ?. ?=(%n -.s) ;span; + ;span.timestamp(timestamp (trip p.s)):"" + %note ;span; + %break ;br; + %underline ;span.underline:"{(trip p.l)}" + %sup ;sup:"{(trip p.l)}" + %sub ;sub:"{(trip p.l)}" + %ruby ;ruby + "{(trip p.l)}" + ;rt:"{(trip q.l)}" + == +== +++ parse-text +|= txt=@t ^- manx +=/ tpe (trip txt) +=/ youtube (rush txt youtube:parsing:sr) +?^ youtube +:: ;a/"{tpe}" +:: ;img@"https://i.ytimg.com/vi/{u.youtube}/hqdefault.jpg"; +:: == +;iframe.youtube-frame@"https://www.youtube.com/embed/{u.youtube}"; +=/ twatter-status (rush txt twatter:parsing:sr) +?^ twatter-status +;div :: goddamn twatter embeds insert themselves as last child + ;a.parsed-twatter(status u.twatter-status):"{tpe}" +== +=/ trimmed (rush txt trim:parsing:sr) +?~ trimmed ~& parsing-error=txt ;span.parsing-error; +=/ link=(unit purl:eyre) (rust u.trimmed link:parsing:sr) +?^ link + ?^ p.q.u.link + ?: (is-img u.p.q.u.link) ;img@"{u.trimmed}"; + ;a.parsed-link/"{tpe}"(target "_blank"):"{tpe}" :: normal link + ;a.parsed-link/"{tpe}"(target "_blank"):"{tpe}" :: normal link +;span:"{tpe}" + +++ is-img +|= t=@ta +=/ img-set %- silt +:~ ~.webp + ~.png + ~.jpeg + ~.jpg +== +(~(has in img-set) t) +-- diff --git a/desk/web/components/post.hoon b/desk/web/components/post.hoon new file mode 100644 index 0000000..488c308 --- /dev/null +++ b/desk/web/components/post.hoon @@ -0,0 +1,274 @@ +/- tp=trill-post +/+ plib=trill-utils, sr=sortug, kaji, lib=boke +/= content /web/components/post-text +/= post-header /web/components/post-header +|_ [[=post:tp children=full-graph:tp] who=@p] +++ css ^~ %- trip +''' +#post-text{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + line-height: 1.618; + text-align: left; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + border-bottom-color: rgb(223, 223, 223); + border-bottom-style: dotted; + border-bottom-width: 1px; + box-sizing: border-box; + display: block; + margin-bottom: 24px; + margin-left: 0px; + margin-right: 0px; + margin-top: 0px; + padding-bottom: 40px; + padding-left: 32px; + padding-right: 32px; + padding-top: 4px; +} +#comments{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + line-height: 1.618; + text-align: left; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + box-sizing: border-box; + display: block; + margin-bottom: 32px; + padding-bottom: 0px; + padding-left: 16px; + padding-right: 16px; + padding-top: 0px; +} +#comments h3{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + text-align: left; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + font-size: 18.72px; + font-weight: 400; + box-sizing: border-box; + display: block; + line-height: 1.2; + margin-block-end: 9.36px; + margin-block-start: 9.36px; + margin-bottom: 9.36px; + margin-inline-end: 0px; + margin-inline-start: 0px; + margin-left: 0px; + margin-right: 0px; + margin-top: 9.36px; +} +#comment-list{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + line-height: 1.618; + text-align: left; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + box-sizing: border-box; + display: block; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + margin-block-end: 24px; + margin-block-start: 0px; + margin-bottom: 24px; + margin-inline-end: 0px; + margin-inline-start: 0px; + margin-left: 0px; + margin-right: 0px; + margin-top: 0px; + padding-inline-start: 0px; + padding-left: 0px; +} +.comment-list li{ + -webkit-locale: "en"; + text-size-adjust: 100%; + color: rgb(0, 0, 0); + line-height: 1.618; + text-shadow: rgb(0, 0, 0) 0px 0px 0px; + list-style-image: none; + list-style-position: outside; + list-style-type: none; + border-left-color: rgb(223, 223, 223); + border-left-style: solid; + border-left-width: 1px; + box-sizing: border-box; + display: list-item; + padding-bottom: 16px; + padding-left: 10px; + padding-right: 0px; + padding-top: 16px; + text-align: left; +} +.comment-author{ + font-weight: 700; + color: var(--text); + font-style: normal; + font-size: 1rem; + margin-right: 1rem; +} +.comment-content{ + font-size: 90%; +} +#reply-title{ + display: inline; +} +.reply-prompt{ + color: var(--hong); + font-size: 0.9rem; +} +''' +++ script %- trip +''' +document.addEventListener('alpine:init', () => { + console.log(Alpine, Date.now()) + Alpine.store("comment", { + text: "", + set(s){ + this.text = s + } + }); + Alpine.store("replying_to", { + show(s){ + return this.to === s + }, + to: "", + open(s){ + this.to = s + } + }); + Alpine.bind('bigReplyButton', () => ({ + type: 'button', + 'x-on:click'(){ + Alpine.store('replying_to').open('main'); + } + })); + Alpine.bind('ReplyButton', () => ({ + type: 'button', + 'x-on:click'() { + Alpine.store("replying_to").open(this.$el.id) + } + })); + Alpine.bind("cancelButton", () => ({ + 'x-on:click'() { + Alpine.store("replying_to").open("") + } + })) + Alpine.bind("collapseButton", () => ({ + 'x-on:click'() { + Alpine.store("replying_to").open("") + } + })) +}) +''' +++ post-content +^- manx +;div#post-text + ;style:"{css}" + ;* (content contents.post) +== +++ comments +=/ cs (tap:form:tp children) +=/ l (houyi:plib children) +=/ length ?: .=(0 l) "No" "{<l>}" +=/ js "$store.replying_to.show('main')" +^- manx +;div#comments + ;a/"/board/p/{(board-path:lib post)}":"Switch to Board View" + ;h3:"{length} comments" + ;span.fl(x-bind "bigReplyButton"):"Leave a reply" + ;div(x-show js) + ;+ (reply-box-f [author.post id.post]) + == + ;ul.comment-list + =id "children{(scow %ud id.post)}" + ;* (turn cs comment) + == +== +++ grandchildren +|= p=full-node:tp ^- manx +=/ cs (tap:form:tp children.p) +;ul.comment-list + =id "children{(scow %ud id.p.p)}" + ;* (turn cs comment) +== +++ comment +|= [=pid:tp [p=post:tp c=full-graph:tp]] +^- manx +=/ author (get-name:lib -.pid) +=/ time (datetime-to-tape:string:sr id.pid "/") +=/ id=tape "{<`@ud`id.pid>}" +=/ js=tape "$store.replying_to.show('{id}')" +:: =/ uncollapse=manx ;span.cp(x-bind "collapseButton"):"[+] ({<(houyi p)>})" +:: =/ collapse ;span.cp(x-bind "collapseButton"):"[-]" +;li + =id "comment{id}" + ;div.comment-meta + ;div.row + ;a.comment-author/"/u/{author}":"{author}" + ; | + ;a.comment-time/"{time}":"{time}" + ; | + == + == + ;div.comment-content + ;* (content contents.p) + == + ;div(x-show js) + ;+ (reply-box-f pid) + == + ;span.fl.reply-prompt(id id, x-bind "ReplyButton"):"reply" + ;+ (grandchildren [p c]) +== +++ reply-box-f +|= =pid:tp +?: .=(%pawn (clan:title who)) +;p#respond:"You must login with an Urbit ID to comment" +(reply-box pid) +++ reply-box +|= =pid:tp +^- manx +=/ post-meta (enc:kaji [[author.post id.post] pid]) +=/ parent-id=tape "{<`@ud`id.pid>}" +=/ author %- trip (scot %p who) +;div#respond + ;span + =id "cancel-reply" + =class "fl" + =x-bind "cancelButton" + ; Cancel reply + == + ;form#reply-form + =kaji "poke" + =action "add-post" + ;label(for "comment"); + ;textarea#comment + =name "comment" + =cols "45" + =rows "8" + =maxlength "6552" + =autocomplete "off" + ; + == + ;div#author-data + ;span:"Posting as " + ;span:"{author}" + == + ;input(type "hidden", name "post-meta", value post-meta); + ;input#submit-comment(type "submit", value "Poast"); + == +== +++ html ^- manx +~& post +~& >> children + ;div + ;+ (post-header post) + ;+ post-content + ;+ comments + ;script:"{script}" + == +-- diff --git a/desk/web/components/sidebar.hoon b/desk/web/components/sidebar.hoon new file mode 100644 index 0000000..f0fa536 --- /dev/null +++ b/desk/web/components/sidebar.hoon @@ -0,0 +1,38 @@ +/- tp=trill-post +/+ *sortug +=< html +|% +++ css ^~ %- trip +''' +#sidebar{ + width: 25%; +} +''' +++ recent-comments +:: =/ comments ;; (list [c=post:tp p=post:tp]) (retrieve /recent-comments bowl) +;section#recent-comments + ;h6.widget-title:"Recent Comments" + :: ;ul + :: ;* %+ turn comments |= [c=post:tp p=post:tp] ^- manx + :: =/ author "" + :: =/ excerpt "" + :: ;li + :: ;span.comment-author-link:"{author}" + :: ;span:"on" + :: ;a/"{path.p}":"{(trip title.p)}" + :: ;span.excerpt:"{excerpt}" + :: == + :: == +== +++ html + ;div#sidebar + ;style:"{css}" + ;div#searchbox + ;input(type "text"); + :: bestposts + :: recentposts + ;+ recent-comments + :: archivedropdown + == + == +--
\ No newline at end of file diff --git a/desk/web/components/thread-preview.hoon b/desk/web/components/thread-preview.hoon new file mode 100644 index 0000000..758f6d3 --- /dev/null +++ b/desk/web/components/thread-preview.hoon @@ -0,0 +1,70 @@ +/- tp=trill-post, cnt=contact +/+ lib=boke, sigil=sigil-sigil, sr=sortug, plib=trill-utils, ui=trill-ui, wall +/= user /web/components/user + +|_ [t=thread:tp contacts=whoms:cnt =bowl:gall] ++* wal ~(. wall src.bowl) +++ is-blog (~(has in tags.t) 'blog') +++ pat %- trip %- spat ?: is-blog (blog-path:lib t) (weld /board path.t) +++ wide +:: =/ blocks (abbreviate-post:plib content.p.fn 500) +=/ u (user ship.pid.t contacts 40) +=/ reply-count (scow:parsing:sr %ud (lent replies.t)) +=/ ttags (tags:wal ~(tap in tags.t)) +;div.thread-preview + ;div.thread-author + ;+ -.u + ;+ +.u + == + ;div.thread-name + ;div + ;a/"{pat}" + ;h2:"{(trip title.t)}" + == + ;div.thread-tags + ;* %+ turn ttags |= tg=@t ;a.tag/"/board?t={(trip tg)}": {(trip tg)} + == + == + == + ;div.reply-count + ;div:"Posted {(post-date-ago:lib id.pid.t now.bowl %tam)} ago" + ;div:"{reply-count} replies" + == + ;div.dates + ;* last-reply + == +== +++ last-reply ^- marl + ?~ replies.t ~ + =/ usr (user ship.i.replies.t contacts 40) + ;+ ;div.last-reply + ;span: Last reply {(post-date-ago:lib id.i.replies.t now.bowl %tam)} ago + ;div.last-reply-author + ;span: by + ;+ name.usr + == + == +++ mobile + =/ usr (user ship.pid.t contacts 40) + =/ reply-count (scow:parsing:sr %ud (lent replies.t)) + +;a.thread-preview/"{pat}" +;div.thread-inner + ;h2:"{(trip title.t)}" + ;div.meta + ;div.author + ;div.by-author + ;span:"by" + ;+ +.usr + == + ;div:"{(post-date-ago:lib id.pid.t now.bowl %yau)} ago" + == + ;* ?~ replies.t ~ + ;+ ;div.reply + ;div:"{reply-count} replies" + ;div: Last {(post-date-ago:lib id.i.replies.t now.bowl %tam)} ago + == + == +== +== +-- diff --git a/desk/web/components/user.hoon b/desk/web/components/user.hoon new file mode 100644 index 0000000..871c029 --- /dev/null +++ b/desk/web/components/user.hoon @@ -0,0 +1,28 @@ +/- cnt=contact +/+ sigil=sigil-sigil, lib=boke +/+ lib=boke +|_ [u=@p =whoms:cnt avatar-size=@ud] +++ $ ^- [avatar=manx name=manx] +=/ ming (get-name:lib u) +=/ sig sigil(size avatar-size) + =/ sigl ?: (lth u (bex 64)) + ;div.avatar ;+ (sig u) == + =/ random (random-avatar:lib (jam u)) + ;img.avatar@"{random}"; +=/ nam ;div.name:"{ming}" + +=/ prof (~(get by whoms) [%.y u]) +?~ prof [sigl nam] +=/ avatar + =/ avatar-data (~(get by info.u.prof) %avatar) + ?~ avatar-data sigl + ?. ?=(%look -.u.avatar-data) sigl + ;img.avatar@"{(trip +.u.avatar-data)}"; +=/ name-div + =/ name-data (~(get by info.u.prof) %nickname) + ?~ name-data nam + ?. ?=(%text -.u.name-data) nam + ;div.name:"{(trip +.u.name-data)}" + +[avatar name-div] +-- diff --git a/desk/web/components/welcome.hoon b/desk/web/components/welcome.hoon new file mode 100644 index 0000000..c3c07f5 --- /dev/null +++ b/desk/web/components/welcome.hoon @@ -0,0 +1,20 @@ +=< html +|% +++ css ^~ %- trip +''' +#welcome{ +height: 114px; +text-align: center; +border-bottom-color: rgb(223, 223, 223); +border-bottom-style: dotted; +border-bottom-width: 1px; +} +''' +++ html + |= who=@p + ;div#welcome + ;style:"{css}" + ;h4:"Welcome to the all-new Spandrell site." + ;h4:"{(trip (scot %p who))}" + == +--
\ No newline at end of file diff --git a/desk/web/editor.hoon b/desk/web/editor.hoon new file mode 100644 index 0000000..f07d351 --- /dev/null +++ b/desk/web/editor.hoon @@ -0,0 +1,262 @@ +/- sur=boke, tp=trill-post +/+ ui=trill-ui, sr=sortug, plib=trill-utils, kaji, lib=boke +|_ [[=thread:tp =post:tp children=full-graph:tp] s=state:sur =bowl:gall] +++ css ^~ %- trip +''' +#admin{ + display:flex; + #editor{ + fieldset{ + border: none; + } + input[type=text]{ + width: 100%; + line-height: 1.5rem; + } + + textarea{ + width: 100%; + height: 50vh; + resize: none; + } + } + #sidebar{ + width: 30%; + height: 100vh; + overflow-y: scroll; + } + #right{ + width: 70%; + } + .admin-post{ + } + #comments{ + textarea{ + width: 100%; + height: 10rem; + } + } +} +''' +++ script +=/ json-tags=json %- pairs:enjs:format + %+ turn ~(tap by tags.s) |= [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); +} + + +''' +++ form ^- manx +=/ is-edit !.=(*@da id.post) +=/ tag-string (tags-to-tape:ui tags.post) +=/ post-text (content-to-md:ui contents.post) +;div#editor + ;style:"{css}" + ;a/"/bianji" + ;h3:"编辑器" + == + ;form + =kaji "poke" + =action "add-thread" + ;* ?. is-edit ~ + ;+ ;input(type "hidden", name "editing", value (enc:kaji [~docteg-mothep id.post])); + ;fieldset + ;label:"title" + ;input(type "text", name "title", value (trip title.thread)); + == + ;fieldset + ;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 tag-string); + == + == + ;fieldset + ;textarea(name "text") + ; {post-text} + == + == + ;span#error-div; + ;input(type "hidden", name "error-div", value "#error-div"); + ;input(type "hidden", name "is-blog", value "is"); + ;+ ?. is-edit + ;fieldset + ;input(type "submit", value "Submit"); + == + ;fieldset.fl + ;input(type "submit", value "Edit"); + ;button + =kaji "poke" + =action "del-thread" + =name "pid" + =payload (enc:kaji pid.thread) + ; Delete + == + == + == + ;script:"{script}" +== +++ comments +;div#comments + ;+ ?~ children ;h3:"No comments" + ;div + ;h3:"{<(houyi:plib children)>} comments" + ;* %+ turn (tap:form:tp children) comment + == +== +++ comment +|= [=pid:tp p=full-node:tp] +^- manx +=+ [sip id]=pid +=/ author (get-name:lib sip) +=/ time (datetime-to-tape:string:sr id.p.p "/") +=/ id=tape (scow:parsing:sr %ud id) +=/ js=tape "$store.replying_to.show('{id}')" +:: =/ uncollapse=manx ;span.cp(x-bind "collapseButton"):"[+] ({<(houyi p)>})" +:: =/ collapse ;span.cp(x-bind "collapseButton"):"[-]" +;li + =id "comment{id}" + ;div.comment-meta + ;div.row + ;a.comment-author/"{author}":"{author}" + ; | + ;a.comment-time/"{time}":"{time}" + ; | + ;span.toggle + =target "comment-tree-{id}" + ; [-] + == + ;button + =kaji "poke" + =action "del-post" + =name "pid" + =payload (scow:parsing:sr %uw (jam pid)) + ; Delete + == + == + == + ;form.comment-content + =kaji "poke" + =action "edit-post" + ;textarea + =name "comment" + ; {(content-to-md:ui contents.p.p)} + == + ;input(type "hidden", name "id", value id); + ;input(type "hidden", name "error-div", value "#error-div"); + ;input(type "hidden", name "ship", value (scow %p sip)); + ;input(type "submit", value "Edit"); + == + :: ;span.fl(id id, x-bind "ReplyButton"):"reply" + ;+ (grandchildren p) +== +++ grandchildren +|= p=full-node:tp ^- manx +:: ?~ children.p ;ul.comment-list; +=/ cs=(list [pid:tp full-node:tp]) (tap:form:tp children.p) +;ul.comment-list + =id "comment-tree-{(scow:parsing:sr %ud id.p.p)}" + ;* (turn cs comment) +== +++ post-list ^- manx + =/ sorted %+ sort ~(tap by paths.s) + |= [[* p1=pid:tp] [* p2=pid:tp]] (gth id.p2 id.p1) + ;div#sidebar + ;* %+ roll sorted |= [[pat=path =pid:tp] acc=marl] + =/ ted (get:torm:tp threads.s pid) + ?~ ted acc + ?. (~(has in tags.u.ted) 'blog') acc + :_ acc + ;div.admin-post.fl + ;a/"/bianji{(trip (spat pat))}":"{(trip title.u.ted)}" + == + == +++ $ ^- manx +;div#admin + ;+ post-list + ;div#right + ;+ form + ;+ comments + == + ;script:"{collapsible:js:kaji}" +== +-- diff --git a/desk/web/feed/feed.hoon b/desk/web/feed/feed.hoon new file mode 100644 index 0000000..fafcb7e --- /dev/null +++ b/desk/web/feed/feed.hoon @@ -0,0 +1,47 @@ +/- boke, tp=trill-post, tlonc=tlon-channels, cnt=contact +/+ plib=trill-utils, ui=trill-ui, sr=sortug, lib=boke, const=constants, kaji +/= post-text /web/components/post-text +/= date-div /web/components/date +/= user /web/components/user +/= chat-manx /web/chat/chat +|_ [s=state:boke =bowl:gall] +++ css ^~ %- trip +''' +ddEventListener('kaji-scry', async () => highlight()); +''' ++$ section $?(%blog %comments %chat %threads %replies) +++ cursor +|= [d=(unit time) up=?] ^- marl + ?~ d ~ + =/ cursor-string (scow:parsing:sr %uw `@uw`u.d) + =/ name ?: up "before" "after" + =/ label ?: up "Newer Posts" "Older Posts" + =/ indicator ?: up "#spinner-up" "#spinner-down" + =/ button=manx + ;button + =kaji "iscroll" + =name name + =value cursor-string + =path "/search/f" + =cont "#search-results" + =indicator indicator + ; {label} + == + =/ upc + ;div + ;+ button + ;img#spinner-up.spinner@"spinner.svg"(style "display: none;"); + == + =/ downc + ;div + ;img#spinner-down.spinner@"spinner.svg"(style "display: none;"); + ;+ button + == + ;+ ?: up upc downc + +++ main ^- manx +;div#feed.fsy + ;style: {css} + ;h1.tc:"Feed" +== +-- diff --git a/desk/web/feed/router.hoon b/desk/web/feed/router.hoon new file mode 100644 index 0000000..9c61b4b --- /dev/null +++ b/desk/web/feed/router.hoon @@ -0,0 +1,39 @@ +/- boke, tp=trill-post, cnt=contact +/+ kaji, fetch-lib=fetch, plib=trill-utils, const=constants, sr=sortug, lib=boke +/= index /web/index +/= subscribe /web/subscribe +/= feedp /web/feed/feed + +|_ [rl=req-line:kaji s=state:boke =bowl:gall] ++* fetch ~(. fetch-lib [s bowl]) + feed ~(. feedp [s bowl]) +++ eyre-bail (error-response:kaji 404) +++ manx-bail (error-page:kaji 404) +:: +++ $ ^- eyre-res:kaji + =/ p pat.rl ::?. mob.rl pat.rl [%m pat.rl] + ?. (is-subscribed:lib src.bowl) nudge + ~& serving-search=rl(pat p) + ?+ p eyre-bail + ~ main + [%f rest=*] (fragment rest.p) + == +++ nudge + :- %page + ~& "pay me" + =/ sub ~(. subscribe src.bowl) + =/ nudgep (nudge:sub "Feed") + (index ~[nudgep] bowl) +++ main + :- %page + + (index ~[main:feed] bowl) +++ fragment +|= p=(pole knot) + :- %html manx-bail + :: =/ args (parse-params par.rl) + :: ~& args=args + :: ?~ args manx-bail + :: =/ res (search:fetch u.args) + :: (results:srch u.args res) +-- diff --git a/desk/web/index.css b/desk/web/index.css new file mode 100644 index 0000000..d41d32b --- /dev/null +++ b/desk/web/index.css @@ -0,0 +1,295 @@ + * { + box-sizing: border-box; + } + + /*@font-face { + font-family: 'Arvo'; + src: url('/fonts/arvo-regular.ttf') format('truetype'); + } + + @font-face { + font-family: 'Crimson Text'; + src: url('/fonts/crimsontext-regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + }*/ + + :root { + --red05: rgba(255, 65, 54, 0.05); + --red100: rgba(255, 65, 54, 1); + --blue05: rgba(33, 157, 255, 0.05); + --blue30: rgba(33, 157, 255, 0.3); + --blue100: rgba(33, 157, 255, 1); + --black05: rgba(0, 0, 0, 0.05); + --black20: rgba(0, 0, 0, 0.2); + --black60: rgba(0, 0, 0, 0.6); + --white: rgba(255, 255, 255, 1); + --text: rgba(0, 0, 0, 1); + --hong: rgb(141, 15, 15); + --huang: rgb(230, 180, 60); + --lan: rgb(30, 60, 80); + } + + [data-theme=dark] { + --background-color: black; + --text-color: rgb(200, 200, 200); + } + + [data-theme=light] { + /* --background-color: #EFF1EC; */ + --background-color: white; + --text-color: black; + } + + html { + height: 100%; + color: var(--text-color); + -webkit-font-smoothing: antialiased; + } + + body { + margin: 0; + width: 100%; + height: 100%; + font-size: 16px; + font-weight: 400; + line-height: 1.5; + font-size: 1rem; + line-height: 1.618; + text-align: left; + color: black !important; + text-rendering: optimizeLegibility !important; + -webkit-font-smoothing: antialiased; + word-break: break-word; + } + + body>main>header { + border-bottom: 1px solid #eee; + text-align: center; + margin: 1rem auto; + padding: 1rem 0; + } + + h1 { + margin-bottom: 0px; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 40px; + font-weight: 500; + line-height: 1.25; + color: #363636; + margin-top: 0; + } + + h4 { + font-size: 1.4rem; + } + + body>main>header a { + color: var(--text); + text-decoration: none; + background-color: transparent; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin: 0 0 0.5em 0; + font-weight: 400; + line-height: 1.2; + font-family: 'Crimson Text'; + } + + p { + font-family: 'Crimson Text'; + font-size: 20px; + } + + a, + button, + input { + transition: all .4s ease; + } + + a, + .fl { + text-decoration: none; + color: #B70E0E; + cursor: pointer; + } + + body>main { + height: calc(100% - 4rem); + /* minus navbar */ + } + + .error-div, + #error-div { + color: red; + } + + .blog { + text-align: justify; + } + + /* desktop */ + @media (min-width: 935px) { + .blog { + width: 935px; + margin: auto; + } + } + + @media (max-width: 935px) { + body>main { + width: inherit; + } + + .blog { + padding: 0 0.5rem; + } + } + + #content { + flex: 0 0 70%; + max-width: 70%; + } + + input[type="submit"], + button, + .button { + border-color: #181818; + background-color: #181818; + padding: 0.5rem 1.25rem; + color: #fff; + font-size: 0.875rem; + line-height: 1.5; + border-radius: 0.2rem; + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + cursor: pointer; + -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + -o-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; + font-family: 'Montserrat', sans-serif; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 13px; + -webkit-transition: all .4s ease; + -o-transition: all .4s ease; + transition: all .4s ease; + } + + .hs { + white-space: nowrap; + overflow-x: auto; + } + + .cp { + cursor: pointer; + } + + .tabs { + display: flex; + justify-content: space-between; + + & .tab { + cursor: pointer; + padding: 0.2rem; + } + + & .tab.active { + border-bottom: 1px solid var(--red); + } + } + + /* mobile responsive */ + @media (max-width: 768px) { + body>main { + padding: 0.2rem; + } + } + + /* tailwindy stuff */ + .flex { + display: flex; + } + + .ml1 { + margin-left: 1rem; + } + + .ml2 { + margin-right: 1rem; + } + + .fhns { + height: 100%; + overflow: hidden; + } + + .fh { + height: 100%; + } + + .sy { + overflow-y: scroll; + } + + .fsy { + overflow-y: scroll; + height: 100%; + + } + + .fxc { + display: flex; + justify-content: center; + align-items: baseline; + } + + .f1 { + display: flex; + justify-content: space-between; + align-items: center; + } + + .gc { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100; + } + + .axc { + position: absolute; + left: 50%; + transform: translateX(-50%); + } + + .xc { + display: block; + margin-left: auto; + margin-right: auto; + } + + .tc { + text-align: center !important; + } + + .bb { + border-bottom: 1px solid var(--text-color); + }
\ No newline at end of file diff --git a/desk/web/index.hoon b/desk/web/index.hoon new file mode 100644 index 0000000..7684289 --- /dev/null +++ b/desk/web/index.hoon @@ -0,0 +1,31 @@ +/- tp=trill-post +/+ *sortug +/= nav /web/layout/nav +/= footer /web/layout/footer +/= sidebar /web/components/sidebar +/* css %css /web/index/css +|_ [children=marl =bowl:gall] +++ $ + :: ;link(rel "icon", type "image/svg+xml", href (weld "data:image/svg+xml;utf8," favicon)); +^- manx + ;html + =data-theme "light" + ;head + ;meta(charset "utf-8"); + ;meta(name "viewport", content "width=device-width, initial-scale=1, shrink-to-fit=no"); + ;title:"Bloody Shovel 4" + ;style:"{(trip css)}" + ;script@"https://platform.twitter.com/widgets.js"(async "", charset "utf-8"); + ;link/"https://fonts.googleapis.com"(rel "preconnect"); + ;link/"https://fonts.gstatic.com"(rel "preconnect"); + ;link/"https://fonts.googleapis.com/css2?family=Arvo&family=Crimson+Text&family=Inter&display=swap"(rel "stylesheet"); + == + ;body + ;+ (nav bowl) + ;main + ;* children + == + :: ;+ footer + == + == +-- diff --git a/desk/web/invoice.hoon b/desk/web/invoice.hoon new file mode 100644 index 0000000..b3529c7 --- /dev/null +++ b/desk/web/invoice.hoon @@ -0,0 +1,87 @@ +|% +++ css ^~ %- trip +''' +body{ + width: 600; + margin: auto; + height: 100%; + overflow-y: hidden; +} +.row{ + display: flex; +} +.meta{ + justify-items: right; +} +''' +++ logo "https://s3.sortug.com/img/sortug-logo.png" +++ html +;html +;body +;style: {css} + ;div.row + ;div.logo + ;div:"Sortug Development" + ;img@"{logo}"; + == + ;h1: Invoice + == + ;div.row + ;div.dest + ;div:"Billed to:" + ;div:"Chorus One AG" + == + ;div.meta + ;div:"Invoice #20240205" + ;div:"Issued 2024/Feb/21" + ;div:"Due: On receipt" + == + == + ;table.items + ;tr + ;th:"Description" + ;th:"Quantity" + ;th:"Price" + ;th:"Amount" + == + ;tr.item + ;td.desc + ;div:"Urbit Development" + ;span:"Development of Tendermint implementation for %chain project" + == + ;td:"1" + ;td:"USD 2,500" + ;td:"USD 2,500" + == + == + ;div.row + ;div.payment + ;h2:"Payment Method" + ;div:"Bank name: HSBC Hong Kong" + ;div:"Bank address: 1 Queen's Road Central, Hong Kong" + ;div:"SWIFT code: HSBCHKHHHKH" + ;div:"Account number: 121-338933-833" + ;div:"Account name: Santiago Campos Losada" + == + ;table.total + ;tr + ;td:"Subtotal" + ;td:"USD 2,500" + == + ;tr + ;td:"Tax" + ;td:"-" + == + ;tr + ;td:"Discount" + ;td:"-" + == + ;tr.total + ;td:"Total due" + ;td:"USD 2,500" + == + == + == +== +== +-- diff --git a/desk/web/layout/footer.hoon b/desk/web/layout/footer.hoon new file mode 100644 index 0000000..dd12b8e --- /dev/null +++ b/desk/web/layout/footer.hoon @@ -0,0 +1,22 @@ +=< html +|% +++ css ^~ %- trip +''' +body>footer{ + text-align: center; + background: #181818; + color: rgba(255, 255, 255, 0.6); + padding: 0.5rem 0; + font-size: 90%; + letter-spacing: 0.5px; + border-top: 2px solid rgba(0, 0, 0, 0.15); + text-align: center; +} +''' +++ html + ;footer + ;style:"{css}" + ;div:"©︎ Spandrell 2011-2024 All rights reserved." + ;p:"Powered by %kaji" + == +-- diff --git a/desk/web/layout/nav.hoon b/desk/web/layout/nav.hoon new file mode 100644 index 0000000..871ab69 --- /dev/null +++ b/desk/web/layout/nav.hoon @@ -0,0 +1,310 @@ +/+ sigil=sigil-sigil +/* global-css %css /web/index/css +|_ =bowl:gall +++ css ^~ %- trip +''' +body > nav { + box-shadow: inset 0 2px 4px rgba(30, 30, 30, 0.15); + position: sticky; + top: 0; + z-index: 1020; + clear: both; + display: flex; + justify-content: space-between; + width: 100%; + height: 4rem; + text-align: center; + background: black; + color: rgb(220, 220, 220); + padding: 0 1rem; + z-index: 100; + box-sizing: border-box; + + & #top-left { + margin: 1rem 0; + color: inherit; + width: 60px; + } + & .menu{ + height: 55px; + } +& .option{ + display: block; + text-decoration: none; + color: #fff; + text-transform: uppercase; + font-family: 'Montserrat', sans-serif; + font-weight: 500; + font-size: 13px; + letter-spacing: 0.5px; + margin: 1.2rem 0.5rem; +} + + + & ul { + display: flex; + list-style: none; + margin: 0; + padding-left: 0; + position: absolute; + top: 50%; /* position the top edge of the element at the middle of the parent */ + left: 50%; /* position the left edge of the element at the middle of the parent */ + transform: translate(-50%, -50%); /* This is a shorthand of + translateX(-50%) and translateY(-50%) */ + + & li{ + margin: 0 0.5rem; + + & a, nav>form a { + display: block; + text-decoration: none; + color: #fff; + text-transform: uppercase; + font-family: 'Montserrat', sans-serif; + font-weight: 500; + font-size: 13px; + letter-spacing: 0.5px; + } + } + } + & a:hover { + color: rgba(255, 255, 255, 0.7); + } + + /* icon links style + & svg{ + fill: white; + } + & path{ + fill: white; + } + & circle{ + stroke: white; + } + & svg{ + float: left; + margin-top: 10px; + margin-left: 10px; + } + & .nav-right svg{ + height: 4rem; + border-left: 1px solid white; + border-right: 1px solid white; + margin-left: 0.5rem; + } */ + & .icon{ + width: 30px; + } + & form{ + background-color: var(--background-color); + margin: 0.7rem; + padding: 0 0.3rem; + } + & input[type=text]{ + background-color: var(--background-color); + outline: none; + padding: 0.3rem; + border: none; + } + + @media (max-width:935px) { + & input[type=text]{ + display: none; + } + & form{ + background-color: inherit; + width: 50px; + } + & .icon{ + filter: invert(); + } + } + & .sigil{ + padding: 6px 0; + } + +} + +''' +++ mobile-css ^~ %- trip +''' +body > nav{ + padding: 5px 1rem; + & #logo{ + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 25%; + } + & #menu-button{ + width: 30px; + float: right; + } + #nav-menu{ + background-color: black; + position: fixed; + top: 25px; + right: 25px; + height: max-content; + width: 40%; + + & a{ + display: block; + padding: 5px; + color: white; + } + } +} +''' +++ smol +;nav + ;style:"{css}" + ;style:"{mobile-css}" + ;a/"/" + ;img#logo@"https://s3.spandrell.ch/assets/board/ui/tianming.svg"; + == + ;img#menu-button@"https://s3.spandrell.ch/assets/board/ui/menu.svg" + =kaji "toggle" + =targ "#nav-menu" + ; + == + ;div#nav-menu.cp + =role "link" + =kaji "clickaway" + =hidden "" + ;a/"/blog":"BLOG" + ;a/"/chat":"CHAT" + ;a/"/board":"BOARD" + ;a/"/feed":"FEED" + ;a/"/tv":"TV" + ;a/"/wiki":"WIKI" + :: ;a/"/apps/canvas":"CANVAS" + ;a/"/book":"BOOKS" + ;a/"/about":"ABOUT" + ;a/"/search":"SEARCH" + ;+ ?: .=(%pawn (clan:title src.bowl)) + ;a/"/login":"LOGIN" + ;a/"/user":"PROFILE" + == +== +:: TODO figure this out +++ ext ^- manx + =/ sip (scow %p src.bowl) + =/ dap (trip dap.bowl) + ;nav + =kaji "mobile" + =path "/f/mobile-nav" + =hide "clickaway" + ;style:"{css}" + ;style:"{(trip global-css)}" + ;a#top-left/"/" + ;img@"https://s3.spandrell.ch/assets/board/ui/tianming.svg"; + == + ;ul + ;li + ;a/"/blog":"BLOG" + == + ;li + ;a/"/chat":"CHAT" + == + ;li + ;a/"/board":"BOARD" + == + ;li + ;a/"/feed":"FEED" + == + ;li + ;a/"/tv":"TV" + == + ;li + ;a/"/wiki":"WIKI" + == + :: ;li + :: ;a/"/apps/canvas":"CANVAS" + :: == + ;li + ;a/"/book":"BOOKS" + == + ;li + ;a/"/about":"ABOUT" + == + == + ;div.flex.nav-right + :: ;form#global-search.f1 + :: =kaji "search" + :: =path "/search" + :: ;input(type "text", name "query"); + :: ;input.icon@"https://s3.sortug.com/img/icons/search-l.svg"(type "image"); + :: == + ;a.option/"/search":"search" + ;+ ?: .=(%pawn (clan:title src.bowl)) + ;a.option + =href "/login" + ; Login + == + ;a.sigil + =href "/user" + ;+ (sigil(size 48) src.bowl) + == + == + ;script@"/kaji.js"; + ;script@"/kaji-session.js?ship={sip}&dap={dap}"; + == +++ $ ^- manx + ;nav + =kaji "mobile" + =path "/f/mobile-nav" + =hide "clickaway" + ;style:"{css}" + ;a#top-left/"/" + ;img@"https://s3.spandrell.ch/assets/board/ui/tianming.svg"; + == + ;ul + ;li + ;a/"/blog":"BLOG" + == + ;li + ;a/"/chat":"CHAT" + == + ;li + ;a/"/board":"BOARD" + == + ;li + ;a/"/feed":"FEED" + == + ;li + ;a/"/tv":"TV" + == + ;li + ;a/"/wiki":"WIKI" + == + :: ;li + :: ;a/"/apps/canvas":"CANVAS" + :: == + ;li + ;a/"/book":"BOOKS" + == + ;li + ;a/"/about":"ABOUT" + == + == + ;div.flex.nav-right + :: ;form#global-search.f1 + :: =kaji "search" + :: =path "/search" + :: ;input(type "text", name "query"); + :: ;input.icon@"https://s3.sortug.com/img/icons/search-l.svg"(type "image"); + :: == + ;a.option/"/search":"search" + ;+ ?: .=(%pawn (clan:title src.bowl)) + ;a.option + =href "/login" + ; Login + == + ;a.sigil + =href "/user" + ;+ (sigil(size 48) src.bowl) + == + == + == +-- diff --git a/desk/web/layout/title.hoon b/desk/web/layout/title.hoon new file mode 100644 index 0000000..7675165 --- /dev/null +++ b/desk/web/layout/title.hoon @@ -0,0 +1,11 @@ +=< html +|% +++ html ^- manx + ;header + ;h1 + ;a/"/":"Bloody Shovel 4" + == + ;h4:"Nemo nos Salvabit" + :: ;h4:"不當而當亂也" + == +--
\ No newline at end of file diff --git a/desk/web/login.hoon b/desk/web/login.hoon new file mode 100644 index 0000000..017df8d --- /dev/null +++ b/desk/web/login.hoon @@ -0,0 +1,82 @@ +|_ who=@p +++ script ^~ %- trip +''' +console.log('oh hai') +''' +++ css ^~ %- trip +''' +#login{ + margin-top: 3rem; + & h1 { + text-align: center; + } + & form{ + margin: auto; + width: 50%; + text-align: center; + + & input[type=text]{ + outline: none; + padding: 0.5rem; + } + & button{ + display: block; + margin: 1rem auto; + } + + } +} +''' +++ $ ^- manx + :: =/ redirect=(unit @t) (get-header:http 'redirect' u.parsed) +:: =/ redirect-str ?~(redirect-url "" (trip u.redirect-url)) + =/ redirect-str "" +;div#login.blog + ;style: {css} + ;h1: Login + ;p: You can read the most of the blog or hang out at the public chat room without an account, but in order to leave comments or join the board you will need to login. + ;p: Bloody Shovel 4 does not keep any credentials from its visitors. There is no registration process. No passwords and emails or phone numbers here. + ;p: You can login with your Urbit ship. Input your Urbit ID below, you will be redirected to your own ship's interface to authorize the login. + ;form(action "/~/login", method "post") + ;p: Urbit ID + ;input.mono(type "text") + =name "name" + =id "name" + =placeholder "~sorreg-namtyv" + =required "true" + =minlength "4" + =maxlength "14" + =pattern "~((([a-z]\{6})\{1,2}-\{0,2})+|[a-z]\{3})"; + ;input(type "hidden", name "redirect", value redirect-str); + ;button(name "eauth", type "submit"):"Login" + == + ;p: If you don't have an Urbit ID please reach out to site@spandrell.ch and we'll help you get one. +== +:: ++ new-session-key +:: |- ^- @uv +:: =/ candidate=@uv (~(raw og (shas %session-key eny)) 128) +:: ?. (~(has by sessions.auth.state) candidate) +:: candidate +:: $(eny (shas %try-again candidate)) +:: :: are there cookies passed with this request? +:: =/ cookie-header=@t +:: %+ roll header-list.request +:: |= [[key=@t value=@t] c=@t] +:: ?. =(key 'cookie') +:: c +:: (cat 3 (cat 3 c ?~(c 0 '; ')) value) +:: :: is there an urbauth cookie? +:: :: +:: ?~ urbauth=(get-header:http (crip "urbauth-{(scow %p our)}") u.cookies) +:: ++ session-cookie-string +:: |= [session=@uv extend=?] +:: ^- @t +:: %- crip +:: =; max-age=tape +:: "urbauth-{(scow %p our)}={(scow %uv session)}; Path=/; Max-Age={max-age}" +:: %- format-ud-as-integer +:: ?. extend 0 +:: (div (msec:milly session-timeout) 1.000) +:: ++ cookie-stuff +:: ['set-cookie' (session-cookie-string session-id |)] +-- diff --git a/desk/web/manx.hoon b/desk/web/manx.hoon new file mode 100644 index 0000000..53e1d08 --- /dev/null +++ b/desk/web/manx.hoon @@ -0,0 +1,16 @@ +/- tp=trill-post +/+ plib=trill-utils +/= post-show /web/blog/post +/= index /web/index +|% +++ post-manx +|= [p=post:tp feed=gfeed:tp who=@p] ^- manx + =/ fn (node-to-full:plib p feed) +~(html post-show [fn who]) +++ layout +|= [body=manx =bowl:gall] ^- manx + (index ~[body] bowl) +++ post +|= [p=post:tp feed=gfeed:tp =bowl:gall] ^- manx +%+ index ~[(post-manx p feed src.bowl)] bowl +-- diff --git a/desk/web/merch.hoon b/desk/web/merch.hoon new file mode 100644 index 0000000..978cd3f --- /dev/null +++ b/desk/web/merch.hoon @@ -0,0 +1,67 @@ +=< merch +|% +++ css ^~ %- trip +''' +section{ + position: relative; + display: flex; + gap: 1rem; + align-items: center; + & img{ + width: 100px; + border: 1px solid black; + } + & .desc{ + .p{ + margin-top: 2.5rem; + } + & a{ + margin: 0 1ch; + } + } +} +''' +++ merch +;div.blog + ;style: {css} + ;h1.tc:"Books" + ;main + ;section + ;a/"https://westmartian.square.site/product/bs-1/1" + ;img@"https://s3.spandrell.ch/assets/book-vol1.jpg"; + == + ;div.desc + ;h2.axc: Bloody Shovel, Volumes 1-2 + ;div.p + ;p: If you would like a physical book with all the posts from this blog up to 2022, our friends at West Martian will sell it to you, in two volumes. Click on the image to go to their website. + ;p: I'm told it makes a wonderful gift, and a great prop at parties. + ;p: You can also find it on Amazon if you feel like paying extra to Jeff Bezos so he can buy a bigger yacht for his second wife. + ;p + ;span: We'll also send you an ebook (in any format you like) for + ;a/"/subscribe":"a yearly subscription." + == + == + == + == + ;section + ;img@"https://s3.spandrell.ch/assets/newbook.webp"; + ;div.desc + ;h2.axc: The History of Right Retreat + ;div.p + ;p + ;span: But there's more to come. An upcoming book, tentatively called "The History of Right Retreat", is in the works, in collaboration with our friends at + ;a/"https://passage.press":"Passage Publishing." + == + ;p: Think of it as a history book elaborating on Biological Leninism. + ;p + ;span: All + ;a/"/subscribe": Subscribers + ;span: to Bloody Shovel 4 will get an exclusive look as the book progresses. Once it done it will be sold at + ;a/"https://passage.press":"the Passage Press book store." + == + == + == + == + == +== +-- diff --git a/desk/web/nag.hoon b/desk/web/nag.hoon new file mode 100644 index 0000000..4c3e94d --- /dev/null +++ b/desk/web/nag.hoon @@ -0,0 +1,21 @@ +|% +++ css ^~ %- trip +''' +a{ + margin: 0 0.5ch; +} +''' +++ html +;div + ;style:"{css}" + ;p: It appears you don't have access to this content. + ;p + ;span: You might have to + ;a/"/login":"Login" + ;span: or + ;a/"/subscribe":"Subscribe" + ;span: to get access + == +== +++ something %hi +-- diff --git a/desk/web/oldrouter.hoon b/desk/web/oldrouter.hoon new file mode 100644 index 0000000..424b44e --- /dev/null +++ b/desk/web/oldrouter.hoon @@ -0,0 +1,130 @@ +/- tp=trill-post +/+ *server, sortug, lib=boke, ui=trill-ui +/= index /web/index +/= post-header /web/components/post-header +/= post-content /web/components/post-text +/= welcome /web/components/welcome +/= post-list /web/components/post-list +|_ =bowl:gall ++$ state +$: %0 + =feed:tp + paths=(map path post:tp) +== ++$ ret [(list card:agent:gall) state] ++$ order [eyre-id=@ta req=inbound-request:eyre] ++$ payloads [p=(list card:agent:gall) q=simple-payload:http] ++$ comment-req [text=@t thread=@da parent=@da author=@p] ++$ new-post [title=@t text=@t author=@p] +++ r +|= [=order =state] ^- ret +~& >> http-req=order +=/ met method.request.req.order +=/ req-line (parse-request-line url.request.req.order) +=/ pth=path :- met site.req-line +~& >>> path=`(list @tas)`pth +=/ pl=[payloads _state] +?+ pth :_ state (serve-404 req-line) + [%'GET' ~ ~] :_ state (serve-index req-line) :: "/" + [%'GET' @ @ @ @ *] :_ state (serve-post site.req-line) + [%'POST' *] (handle-post req-line body.request.req.order state) + :: [%get %blog *] serve-blog + :: [%get 'chat' *] serve-chat + :: [%get 'forum' *] serve-forum + :: [%get 'about' *] serve-about +== +:_ +.pl +%+ weld p.-.pl +%+ give-simple-payload:app eyre-id.order q.-.pl + + +:: TODO send the whole request instead of the req-line +++ serve-index +|= rl=request-line ^- payloads +=/ poasts ;; (list full-node:tp) (retrieve:sortug /index bowl) +:- ~ +%- html-response:gen +(index (welcome src.bowl) (post-list poasts) bowl) +++ serve-post +|= =path ^- payloads +=/ pst ;; full-node:tp (retrieve:sortug (weld /post path) bowl) +=/ post-show ~(. post-content pst src.bowl) +:- ~ +%- html-response:gen +(index (post-header pst) html:post-show bowl) +++ handle-post +|= [rl=request-line body=(unit octs) =state] ^- [payloads _state] +?+ site.rl :_ state (serve-error 404) + [%poast ~] :_ state (serve-error 404) + [%comment ~] (handle-comment body state) +== +++ handle-comment +|= [body=(unit octs) =state] ^- [payloads _state] +=/ cr=(unit comment-req) (parse-comment body) +?~ cr :_ state (serve-error 500) :: TODO not quite but anyway +~& > cr=cr +=/ blog-post (get:orm:tp feed.state thread.u.cr) +?~ blog-post :_ state (serve-error 404) +=/ parent (get:orm:tp feed.state parent.u.cr) +?~ parent :_ state (serve-error 404) +=/ new-post *post:tp +=/ comment-id now.bowl +=/ content-list (tokenize:ui text.u.cr) +?~ content-list [(serve-error 505) state] +=/ contents (gas:corm:tp *content-map:tp ~[[now.bowl content-list]]) +=/ p %= new-post +id comment-id +host our.bowl +author ship+author.u.cr +thread thread.u.cr +parent `parent.u.cr +contents contents +== +~& > p=p +=/ nc (~(put in children.u.parent) comment-id) +=/ npar u.parent(children nc) +=/ nf (put:orm:tp feed.state comment-id p) +=/ nff (put:orm:tp nf id.npar npar) +=/ ns state(feed nff) +=/ url %- crip "{(trip (spat path.u.blog-post))}#comment{<`@`comment-id>}" +~& >>> redirect-url=url :: redirect:gen doesn't change the method wtf +:_ ns `(redirect url) +++ redirect + |= redirect=cord + ^- simple-payload:http + [[303 ['location' redirect]~] ~] +++ parse-comment +|= body=(unit octs) +=/ kvm (handle-html-form:sortug body) +=/ comment (~(get by kvm) 'comment') +?~ comment ~ +=/ thread (~(get by kvm) 'thread') +?~ thread ~ +=/ parent (~(get by kvm) 'parent') +?~ parent ~ +=/ author (~(get by kvm) 'author') +?~ author ~ +=/ t (slaw %ud u.thread) +?~ t ~ +=/ p (slaw %ud u.parent) +?~ p ~ +=/ a (slaw %p u.author) +?~ a ~ +`:*(u.comment u.t u.p u.a) +++ serve-404 +|= =request-line +(serve-error 404) +++ serve-error +|= code=@ud +^- payloads +:- ~ +%- html-response:gen +%- as-octs:mimes:html +%- crip +%- en-xml:html +;html + ;body + ;p:"Error {<code>}" + == +== +-- diff --git a/desk/web/root.hoon b/desk/web/root.hoon new file mode 100644 index 0000000..ceb6e19 --- /dev/null +++ b/desk/web/root.hoon @@ -0,0 +1,91 @@ +/- b=boke, tp=trill-post, c=tlon-channels +/+ fetch-lib=fetch, sr=sortug +/= chat-page /web/chat/chat +|_ [s=state:b =bowl:gall] ++* fetch ~(. fetch-lib [s bowl]) +++ css ^~ %- trip +''' +section{ + display: flex; + width: 100%; + & .a{ + width: 50%; + } +} +h2{ + padding-bottom: 0.2rem; + border-bottom: 1px solid black; + text-align: center; +} +.chat-author{ + margin-right: 1ch; +} +.preview{ + padding: 0 0.8rem; + text-align: left; +} +''' +++ last-blog (thread-page-by-tags:fetch ~['blog'] [~ ~ 5] ~) +++ last-board (thread-page-no-wall:fetch [~ ~ 5] (some |=(t=thread:tp !(~(has in tags.t) 'blog')))) +++ last-chat + =/ pat /chat/(scot %p our.bowl)/chat/posts/newest/5/post + =/ scry ~(scry io:sr bowl) + =/ chat-posts (scry %channels pat paged-posts:c) + (tap:on-posts:c posts.chat-posts) +++ last-feed +:: TODO if gates show only spandrell4 posts +%ok +++ $ +=/ blog last-blog +=/ board last-board +=/ chat last-chat +=/ feed last-feed +;div.blog + ;style:"{css}" + ;div + ;h2: Now playing on Spandrell TV + ;a/"/tv":"Dune Week!" + ;p:"Dune (1984), Dune Part One (2021), Dune (2000 Mini Series), Children of dune (2003 Mini Series)" + == + ;section + ;div.a.blog-activity + ;h2: Last on Blog + ;ul.preview + ;* %+ turn p.blog |= t=thread:tp + ;li + ;a/"{(trip (spat path.t))}":"{(trip title.t)}" + == + == + == + ;div.a.board-activity + ;h2: Last on Board + ;ul.preview + ;* %+ turn p.board |= t=thread:tp + ;li + ;a/"{(trip (spat (weld /board path.t)))}":"{(trip title.t)}" + == + == + == + == + ;section + ;div.a.chat-activity + ;h2: Last on Chat + ;* %+ turn chat |= [time=@da up=(unit post:c)] + ?~ up ;span; =/ memo +<.u.up + =/ author ?: ?=(%pawn (clan:title author.memo)) "anon" (scow %p author.memo) + ;a/"/chat" + ;* %+ turn content.memo |= =verse:c + ?: ?=(%block -.verse) ;span; + ;div.preview.inline + ;span.chat-author:"{author}:" + ;* %+ turn p.verse tinline:chat-page + == + == + == + ;div.a.feed-activity + ;h2: Last on Feed + ;a/"/feed":"Coming soon!" + == + == +== +-- diff --git a/desk/web/router.hoon b/desk/web/router.hoon new file mode 100644 index 0000000..547a2f9 --- /dev/null +++ b/desk/web/router.hoon @@ -0,0 +1,270 @@ +/- *boke, tp=trill-post, cnt=contact, tlonc=tlon-channels +/+ kaji, plib=trill-utils, lib=boke, sr=sortug, const=constants +/= index /web/index +/= nav /web/layout/nav +:: +/= blog /web/blog/router +/= title /web/blog/title +:: +/= board /web/board/router +:: +/= feed /web/feed/router +:: +/= search /web/search/router +:: +/= chat-page /web/chat/chat +:: +/= tv /web/tv/router +/= tvp /web/tv/tv + +/= about /web/about +/= merch /web/merch +:: +:: common +/= modals /web/components/modals +/= cm /web/components/common +:: +/= root /web/root +/= login /web/login +/= subscribe /web/subscribe +/= user /web/user +/= editor /web/editor +:: + +/* spinner %noun /web/assets/spinner/svg +/* favicon %noun /web/assets/favicon/ico +|% +++ eyre-bail (error-response:kaji 404) +++ manx-bail (error-page:kaji 404) ++$ request +$% [%eyre p=eyre-order:kaji] + [%kaji tab=@t req=kaji-req] +== +++ route +|= [r=request =bowl:gall s=state] ^- kaji-res:kaji +|^ + ?- -.r + %eyre :- %res ^- eyre-res:kaji + =/ rl=req-line:kaji (parse-req:kaji req.p.r) + =/ met method.request.req.p.r + =/ fpath=(pole knot) [met pat.rl] + ?: ?=([%apps %canvas rest=*] pat.rl) (serve-canvas rest.pat.rl rl) + ?: ?=([~ @] ext.rl) (serve-assets rl) + :: ?: ?=([[%session ~] [~ %js]] [pat ext]:rl) :- %full (serve-other:kaji %js (crip "window.ship = {(trip (rsh 3 (scot %p our.bowl)))};")) + ?+ fpath eyre-bail + [%'GET' rest=*] (serve-get rl(pat rest.fpath)) + == + %kaji :- %sse ^- sse-res:kaji + ?- -.req.r + %blog-redi (redirect-post post.req.r .y .y) + %board-redi (redirect-post post.req.r .n .n) + %redi (ui-effect [%redi url.req.r]) + %tv-chat (give-tv-chat +.req.r) + %radio-chat (give-radio-chat +.req.r) + %chat-msg (give-chat-msg +.req.r) + == + == +++ kaji-bail :- [%tab ~] :~([%redi '/404']) +++ ui-effect |= e=effect:kaji ^- sse-res:kaji + :- [%tab ~] :~(e) +:: +:: Eyre handling +:: + ++ serve-assets + |= rl=req-line:kaji + ~& serving-assets=rl + ?+ [pat ext]:rl eyre-bail + [[%spinner ~] [~ %svg]] [%mime ['image' 'svg+xml' ~] spinner] + [[%favicon ~] [~ %ico]] [%mime ['image' 'x-icon' ~] favicon] + :: [[%fonts @t ~] [~ %ttf]] ~& pat.rl + :: ~& ~(key by fonts) + :: =/ font (~(got by fonts) 'name') + :: [%mime /application/octet-stream font] + == + + ++ serve-get + |= rl=req-line:kaji ^- eyre-res:kaji + =/ pat pat.rl + ?+ pat ~(show blog [rl s bowl]) + [~ ~] serve-root + [%blog rest=*] (blog [rl(pat rest.pat) s bowl]) + :: + [%board rest=*] (board rl(pat rest.pat) s bowl) + :: + [%feed rest=*] (feed rl(pat rest.pat) s bowl) + :: + [%search rest=*] (search rl(pat rest.pat) s bowl) + :: + [%tv rest=*] (tv rl(pat rest.pat) s bowl) + :: + :: users + [%login rest=*] serve-login + [%subscribe ~] serve-subscribe + [%logout ~] logout + [%user ~] serve-profile + [%user u=@t ~] (serve-user u.pat) + [%chat ~] serve-chat + [%about ~] serve-about + [%book ~] serve-merch + :: admin + [%bianji rest=*] (serve-editor rest.pat) + :: + [%f rest=*] (serve-fragment rest.pat) + == + + + ++ serve-root + :- %page + =/ page (root [s bowl]) + (index ~[title page] bowl) + ++ serve-login + :- %page + (index ~[(login src.bowl)] bowl) + ++ serve-profile :- %page + =/ page (user src.bowl s bowl) + (index ~[page] bowl) + + ++ serve-subscribe + :- %page + (index ~[(subscribe src.bowl)] bowl) + + ++ serve-user + |= suser=@t :- %page + =/ who (slaw %p suser) + ?~ who manx-bail + =/ page (user u.who s bowl) + (index ~[page] bowl) + :: + ++ serve-editor + |= p=path + :- %page + ?. (is-admin:lib src.bowl) manx-bail + =/ bunt [*thread:tp *full-node:tp] + =/ pid (~(get by paths.s) p) + =/ pp ?~ pid bunt + =/ poast (get:gorm:tp feed.s u.pid) + =/ ted (get:torm:tp threads.s u.pid) + ?~ ted bunt ?~ poast bunt + :- u.ted (node-to-full:plib u.poast feed.s) + =/ page=manx (editor pp s bowl) + (index ~[page] bowl) + + ++ serve-chat + :- %page + =/ contacts (get-contacts:cnt bowl) + =/ pat /chat/(scot %p our.bowl)/chat/posts/newest/200/post + =/ scry ~(scry io:sr bowl) + =/ chat-posts (scry %channels pat paged-posts:tlonc) + =/ chat-core ~(. chat-page [contacts bowl]) + =/ page (main:chat-core chat-posts) + (index ~[page] bowl) + + ++ serve-about + :- %page + (index ~[title about] bowl) + ++ serve-merch + :- %page + (index ~[merch] bowl) + + ++ serve-fragment + |= pat=(pole knot) :- %html + ?+ pat manx-bail + [%mobile-nav ~] ~(smol nav bowl) + == + + + ++ logout + :- %full + ^- simple-payload:http + [[303 ['location' '/~/logout'] ['redirect' '/']~] ~] + + :: + :: Kaji handling + :: + ++ redirect-post + |= [=post:tp blog-view=? is-blog=?] ^- sse-res:kaji + =/ ted (get:torm:tp threads.s thread.post) + ?~ ted kaji-bail + =/ pat-string=@t %- spat (get-path:lib path.u.ted blog-view is-blog) + =/ url ?~ parent.post pat-string + =/ hash (enc:kaji [author.post id.post]) + =/ id "comment-{hash}" + (crip "{(trip pat-string)}?goto={id}") + (ui-effect [%redi url]) + + + ++ give-tv-chat + |= [name=@t p=post:tp] + =/ m :: TODO FIX THIS CRAP. inner vs outer html is annoying af + ;div + ;+ (chat-msg:tvp p) + == + =/ effects :~ [%add m '#chat-container' %bottom ~] + [%custom *manx %s 'scroll'] == + :_ effects [%custom ~[/tv/our/[name]]] + ++ give-radio-chat + |= [s=@p p=*] + :: =/ m :: TODO FIX THIS CRAP. inner vs outer html is annoying af + :: ;div + :: ;+ (chat-msg:tvp p) + :: == + :: =/ effects :~ [%add m '#chat-container' %bottom ~] + :: [%custom *manx %s 'scroll'] == + :: :_ effects [%custom ~[/tv/our/(scot %p s)]] + :- [%tab ~] + ~ + + ++ give-chat-msg + |= [=flag:tlonc q=memo:tlonc] + =/ subpath /chat/(scot %p p.flag)/[q.flag] + =/ contacts (get-contacts:cnt bowl) + =/ tlonchat ~(. chat-page [contacts bowl]) + =/ m ;div ;+ (post-div:tlonchat q) == + :- [%custom ~[subpath]] + :~ [%add m '#chat-container' %bottom ~] + [%custom *manx %s 'scroll'] + == + + :: lets play with globs + +$ glob-reference + [hash=@uvH location=glob-location] + :: + +$ glob-location + $% [%http =cord] + [%ames =ship] + == + +$ version + [major=@ud minor=@ud patch=@ud] + +$ glob (map path mime) + +$ href + $% [%glob base=term =glob-reference] + [%site =path] + == + :: glob handling, TODO put in kaji.hoon + ++ serve-canvas + |= [p=(pole knot) rl=req-line:kaji] + =/ pat /charges/canvas + =/ scry ~(scry io:sr bowl) + =/ scry-res (scry %docket pat *) + =/ glob ((soft glob) scry-res) + ?~ glob eyre-bail + =/ glob u.glob + :: ~& glob=~(key by glob) + =/ suffix (weld p (drop ext.rl)) + ?: =(suffix /desk/js) + :+ %mime /text/js + 'window.desk = "canvas";' + =/ requested=path + ?: (~(has by glob) suffix) suffix + /index/html + =/ data=(unit mime) + (~(get by glob) requested) + ?~ data ~& >>> glob-not-found=suffix eyre-bail + :: =/ lmao ?: (cfind:sr 'start' (spat suffix) .n) ~& > `@t`q.q.u.data .n .n + :: =/ clean (replace:string:sr "/apps/canvas" "/canvas" (trip q.q.u.data)) + :: =. q.q.u.data (crip clean) + :: =/ lmao ?: (cfind:sr 'start' (spat suffix) .n) ~& >> `@t`q.q.u.data .n .n + + [%glob u.data] +-- +-- diff --git a/desk/web/search/router.hoon b/desk/web/search/router.hoon new file mode 100644 index 0000000..7e43327 --- /dev/null +++ b/desk/web/search/router.hoon @@ -0,0 +1,112 @@ +/- boke, tp=trill-post, cnt=contact +/+ kaji, fetch-lib=fetch, plib=trill-utils, const=constants, sr=sortug, lib=boke +/= index /web/index +/= search /web/search/search +/= subscribe /web/subscribe +|_ [rl=req-line:kaji s=state:boke =bowl:gall] ++* fetch ~(. fetch-lib [s bowl]) + srch ~(. search [s bowl]) +++ eyre-bail (error-response:kaji 404) +++ manx-bail (error-page:kaji 404) +:: +++ $ ^- eyre-res:kaji + =/ p pat.rl ::?. mob.rl pat.rl [%m pat.rl] + ?. (is-subscribed:lib src.bowl) nudge + ~& serving-search=rl(pat p) + ?+ p eyre-bail + ~ main + [%f ~] (search-fragment par.rl) + == +++ nudge + :- %page + =/ sub ~(. subscribe src.bowl) + =/ nudgep (nudge:sub "Search") + (index ~[nudgep] bowl) +++ main + :- %page + ~& >> params=~(tap by par.rl) + =/ args parse-params + ~& args=args + =/ page ?~ args init:srch + =/ res (search:fetch u.args) + (page:srch u.args res) + (index ~[page] bowl) +++ search-fragment +|= pmap=(map @t @t) + :- %html + =/ args parse-params + ~& args=args + ?~ args manx-bail + =/ res (search:fetch u.args) + (results:srch u.args res) +:: ++ inline-search +:: |= ssection=@t +:: =/ sec ((soft section) ssection) +:: ?~ sec bail +:: =/ args (parse-params par.rl) +:: ~& args=args +:: ?~ args bail +:: =/ res (search:fetch u.sec u.args) +:: (results:srch [u.sec u.args] res) +:: ++ dated-search +:: |= [ssection=@t date=@t] +:: =/ sec ((soft section) ssection) +:: ?~ sec bail +:: =/ interval=@dr +:: ?: .=('day' date) ~d1 +:: ?: .=('week' date) ~d7 +:: ?: .=('month' date) ~d30 +:: ?: .=('year' date) ~d365 ~s1 +:: ?: .=(~s1 interval) bail +:: =/ args (parse-params par.rl) +:: ~& args=args +:: ?~ args bail +:: =/ req=page-req:tp [(some (sub now.bowl interval)) ~ search-page-size:const] +:: =/ res (search:fetch u.sec query.u.args tags.u.args req) +:: (results:srch [u.sec u.args] res) + :: =/ sec (section:srch section) + :: =/ args (parse-params:srch par.rl) + :: ?~ args inline-search-error + :: =/ res (fetch-results:srch sec query.u.args req.u.args) + :: ?~ res inline-search-error + :: =/ =marl (res-to-marl:srch sec query.u.args u.res) + :: ;div + :: ;* marl + :: == + :: ++ inline-search-error + :: %- serve-html:kaji :_ .n %- add-error:kaji + :: %+ alert:kaji "An error ocurred" 3.000 + ++$ section $?(%blog %comments %chat %threads %replies) ++$ pars [=section query=@t tags=(list @t) req=page-req:tp] +++ parse-params ^- (unit pars) + =/ pmap par.rl + =/ query (~(get by pmap) 'query') ?~ query ~ + ?: .=(u.query '') ~ + :: =/ ts (~(get by pmap) 'tags') ?~ ts ~ + =/ =section + =/ ssec (~(get by pmap) 'section') ?~ ssec %blog + =/ tsec ((soft section) u.ssec) ?~ tsec %blog u.tsec + =/ sdr (~(get by pmap) 'interval') + =/ r=page-req:tp [~ ~ search-page-size:const] + =. r ?. ?=(^ sdr) r + =/ interval=@dr + ?: .=('day' u.sdr) ~d1 + ?: .=('week' u.sdr) ~d7 + ?: .=('month' u.sdr) ~d30 + ?: .=('year' u.sdr) ~d365 ~s0 + ?: .=(interval 0) r + r(newer (some (sub now.bowl interval))) + + =/ o (~(get by pmap) 'after') + =/ n (~(get by pmap) 'before') + =/ c (~(get by pmap) 'count') + =? older.r ?=(^ o) (slaw:parsing:sr %uw u.o) + =? newer.r ?=(^ n) (slaw:parsing:sr %uw u.n) + =. count.r ?~ c count.r + =/ cc (slaw %ud u.c) + ?~ cc count.r u.cc + + %- some + [section u.query ~ r] +-- diff --git a/desk/web/search/search.hoon b/desk/web/search/search.hoon new file mode 100644 index 0000000..1cf58d0 --- /dev/null +++ b/desk/web/search/search.hoon @@ -0,0 +1,441 @@ +/- boke, tp=trill-post, tlonc=tlon-channels, cnt=contact +/+ plib=trill-utils, ui=trill-ui, sr=sortug, lib=boke, const=constants, kaji, wall +/= post-text /web/components/post-text +/= date-div /web/components/date +/= user /web/components/user +/= chat-manx /web/chat/chat +|_ [s=state:boke =bowl:gall] ++* wal ~(. wall src.bowl) +++ css ^~ %- trip +''' +#query-string{ + font-weight: 700; + font-size: 1.3rem; +} +#search{ + + & .form{ + + & .row{ + padding: 0.5rem; + border-bottom: 1px solid var(--text-color); + align-items: center; + + & input{ + line-height: 2rem; + flex-grow:1; + } + & h2{ + margin: 0 1rem 0 0; + color: var(--text-color); + } +} + +#filters{ + display: flex; + justify-content: space-between; + padding-top: 0.5rem; + + & select{ + background-color: var(--background-color); + height: 2rem; + } +} + +} + & fieldset{ + display: block; +} + & section{ + margin: 2rem 0; + + & h3 { + font-size: 1.6rem; + } + } + & .res{ + border-bottom: 1px solid var(--text-color); + padding: 0.5rem; + & .name{ + font-weight: 600; + } + & .snip{ + padding: 0 1rem; + } + + /* comments */ + & .comment-meta{ + display: flex; + flex-wrap: wrap; + font-size: 0.9rem; + + & a{ + font-size: 0.9rem; + } + + & .sep{ + margin: 0 0.3rem; + } + } + + & a { + font-size: 1.3rem; + } + & .tag{ + margin: 0 0.3rem; + padding: 0.1rem; + background-color: var(--huang); + opacity: 0.9; + font-size: 0.7rem; + cursor: pointer; + } + } + & .not-found{ + margin-top: 4rem; + } +& .highlight{ + background-color: var(--huang); + } +& button{ + display: block; + margin: 1rem auto; + } +& .spinner{ + width: 50px; + height: 50px; + margin: 1rem auto; + } +& #spinner-zh{ + width: 100px; + height: 100px; +} +& .tabs { + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + border-radius: 5px; + overflow: hidden; + + & .tab { + background-color: #f1f1f1; + float: left; + border: none; + outline: none; + cursor: pointer; + padding: 14px 16px; + transition: 0.3s; + } + + & .tab:hover { + background-color: #ddd; + } + /* Style for active tab */ + & .tab.active { + background-color: #ccc; + } + } +} +@media (min-width: 800px){ + #search{ + width: 70%; + margin: auto; + } +} +''' +++ script ^~ %- trip +''' +function highlight(){ + const params = new URLSearchParams(new URL(window.location.href).search); + const query = params.get('query'); + const div = document.getElementById("search"); + const snips = div.querySelectorAll(".snip"); + const titles = div.querySelectorAll(".title"); + [...titles, ...snips].forEach(el => { + const h = el.innerHTML; + const r = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); + const h2 = h.replace(r, `<span class="highlight">$1</span>`) + el.innerHTML = h2 + }); +} +document.addEventListener("kaji-scry", (e) => highlight()); +addEventListener('DOMContentLoaded', async () => highlight()); +addEventListener('kaji-scry', async () => highlight()); +''' ++$ section $?(%blog %comments %chat %threads %replies) +++ cursor +|= [=pars d=(unit time) up=?] ^- marl + ?~ d ~ + =/ cursor-string (scow:parsing:sr %uw `@uw`u.d) + =/ name ?: up "before" "after" + =/ label ?: up "Newer Posts" "Older Posts" + =/ indicator ?: up "#spinner-up" "#spinner-down" + =/ button=manx + ;button + =kaji "iscroll" + =name name + =value cursor-string + =path "/search/f" + =cont "#search-results" + =indicator indicator + ; {label} + == + =/ upc + ;div + ;+ button + ;img#spinner-up.spinner@"spinner.svg"(style "display: none;"); + == + =/ downc + ;div + ;img#spinner-down.spinner@"spinner.svg"(style "display: none;"); + ;+ button + == + ;+ ?: up upc downc + +++ blog-result +|= [fn=full-node:tp snip=@t] ^- manx + =/ ttags (tags:wal ~(tap in tags.p.fn)) + ;div.res + ;header.f1 + ;div + ;a.title/"{(trip (spat (title-to-path:lib title.p.fn id.p.fn)))}":"{(trip title.p.fn)}" + ;div + ;* %+ turn ttags |= t=@t ;a.tag/"/blog?t={(trip t)}": {(trip t)} + == + == + ;div.post-date + ;* (date-div id.p.fn .y) + == + == + ;div.snip + ;p:"{(trip snip)}" + == + == +++ comment-result +|= [fn=full-node:tp snip=@t contacts=whoms:cnt] ^- manx + =/ thread (get:gorm:tp feed.s thread.p.fn) + ?~ thread ;span; + =/ thread-link=tape (trip (spat (title-to-path:lib title.u.thread id.u.thread))) :: TODO check for relative/absolute path + =/ thread-link=tape (find-board-path:lib paths.s u.thread) + =/ uid (enc:kaji [author.p.fn id.p.fn]) + =/ permalink=tape %+ weld thread-link "/{uid}" + =/ time-string=tape (post-date-ago:lib id.p.fn now.bowl %yau) + =/ usr (user author.p.fn contacts 32) +;div.res + =id "comment-{uid}" + ;div.comment-meta + ;a/"/u/{(scow %p author.p.fn)}" + ;+ name.usr + == + ;span.sep:"|" + ;a/"{permalink}":"{time-string} ago" + ;span.sep:"|" + ;a/"{thread-link}":"On: {(trip title.u.thread)}" + == + ;div.snip + ;p:"{(trip snip)}" + == +== +++ thread-result +|= [fn=full-node:tp snip=@t contacts=whoms:cnt] ^- manx + =/ thread-link=tape (find-board-path:lib paths.s p.fn) + =/ time-string=tape (post-date-ago:lib id.p.fn now.bowl %yau) + =/ usr (user author.p.fn contacts 32) + =/ ttags (tags:wal ~(tap in tags.p.fn)) +;div.res + ;header.f1 + ;div + ;a.title/"/board/p/{thread-link}":"{(trip title.p.fn)}" + ;div + ;* %+ turn ttags |= t=@t ;a.tag/"/board?t={(trip t)}": {(trip t)} + == + == + ;div.post-date:"{time-string} ago" + == + ;+ name.usr + ;div.snip + ;p:"{(trip snip)}" + == +== +++ reply-result +|= [fn=full-node:tp snip=@t contacts=whoms:cnt] ^- manx + =/ thread (get:gorm:tp feed.s thread.p.fn) + ?~ thread ;span; + =/ thread-link=tape (find-board-path:lib paths.s u.thread) + =/ uid (enc:kaji [author.p.fn id.p.fn]) + =/ permalink=tape %+ weld thread-link "/{uid}" + =/ time-string=tape (post-date-ago:lib id.p.fn now.bowl %yau) + =/ ttags (tags:wal ~(tap in tags.p.fn)) + =/ usr (user author.p.fn contacts 32) +;div.res + ;header.f1 + ;div + ;a/"/board/p/{permalink}":"{(trip title.p.fn)}" + ;div + ;* %+ turn ttags |= t=@t ;a.tag/"/board?t={(trip t)}": {(trip t)} + == + == + ;div.post-date:"{time-string} ago" + == + ;+ name.usr + ;div.snip + ;p:"{(trip snip)}" + == +== +:: +++ chat-result +|= [r=reference:tlonc =whoms:cnt] ^- manx +=/ chat-core ~(. chat-manx [whoms bowl]) +?- -.r +%post (post-div:chat-core +<.post.r) +%reply (post-div:chat-core +.reply.r) +== +++ make-params +|= [query=@t res=page-req:tp] +:: =/ newer ?~ newer.res "" "&before={(scow %uw `@uw`u.newer.res)}" +:: =/ older ?~ older.res "" "&after={(scow %uw `@uw`u.older.res)}" +:: =/ count ?: .=(count.res page-size:lib) "" "&count={(scow %ud count.res)}" +:: %- en-urlt:html +:: "?query={(trip query)}{newer}{older}{count}" +"?query={(trip query)}" +++ form +|= =pars +^- manx + ;form.form + =swap "swap" + =targ "#search-results" + =path "/search/f" + =show-params "show" + =indicator "#spinner-zh" + + ;div.flex.row + ;a/"/search" + ;h2:"Search" + == + ;input(type "text", name "query", value (trip query.pars)) + =placeholder "Search all Bloody Shovel Content" + =kaji "search" + =bounce "1000" + ; + == + :: ;fieldset + :: ;label + :: ; Tags + :: ;input(type "text", name "tags"); + :: == + :: == + == + ;div#filters.row + ;label + ; Section + ;select + =name "section" + =kaji "scry" + =path "/search/f" + ;* %+ turn ~["blog" "comments" "threads" "replies" "chat"] |= t=tape + ?: .=(section.pars (crip t)) + ;option + =value t + =selected "" + ; {t} + == + ;option + =value t + ; {t} + == + == + == + ;label + ; Date + ;select + =name "interval" + =kaji "scry" + =path "/search/f" + ;option + =value "all" + ; All time + == + ;option + =value "day" + :: =selected "" + ; Past 24h + == + ;option + =value "week" + ; Last week + == + ;option + =value "month" + ; Last month + == + ;option + =value "year" + ; Last year + == + == + == + == + == ++$ pars [=section query=@t tags=(list @t) req=page-req:tp] +++ chat-cursor +|= [query=@t p=chat-page:boke] ^- marl +~ +++ chatp +|= [query=@t r=page-req:tp p=chat-page:boke] ^- marl +?~ p.p + ;+ ;p.xc:"No results for {(trip query)} on Bloody Shovel Chats" +=/ contacts (get-contacts:cnt bowl) + %+ welp + ;* %+ turn p.p |= ref=reference:tlonc + (chat-result ref contacts) + ;* (chat-cursor query p) + +++ tp-page +|= [=pars p=search-page:boke] ^- marl +?~ res.p ~ + :: ;+ ;p:"No results for {(trip query.pars)} under section {(trip section.pars)}" +=/ contacts (get-contacts:cnt bowl) +?- section.pars + %blog %+ welp + ;* %+ turn res.p |= [fn=full-node:tp snip=@t] + (blog-result fn snip) + ;* (cursor pars older.p .n) + %comments %+ welp + ;* %+ turn res.p |= [fn=full-node:tp snip=@t] + (comment-result fn snip contacts) + ;* (cursor pars older.p .n) + %threads %+ welp + ;* %+ turn res.p |= [fn=full-node:tp snip=@t] + (thread-result fn snip contacts) + ;* (cursor pars older.p .n) + + %replies %+ welp + ;* %+ turn res.p |= [fn=full-node:tp snip=@t] + (reply-result fn snip contacts) + ;* (cursor pars older.p .n) + %chat ~ +== + +++ init ^- manx +;div#search.fsy + ;style: {css} + ;+ (form [%blog '' ~ [~ ~ search-page-size:const]]) + ;img#spinner-zh.gc@"spinner.svg"(style "display: none;"); + ;div#search-results; + ;script: {script} +== +++ page +|= [=pars res=search-res:boke] ^- manx +;div#search.fsy + ;style: {css} + ;img#spinner-zh.gc@"spinner.svg"(style "display: none;"); + ;+ (form pars) + ;* ?: .=(query.pars '') ;+ ;p:"Not found" ~ + ;+ (results pars res) + ;script: {script} +== +++ results +|= [=pars res=search-res:boke] ^- manx + ;div#search-results + ;* ?: ?=(%chat -.res) + (chatp query.pars req.pars +.res) + (tp-page pars +.res) + == +-- diff --git a/desk/web/subscribe.hoon b/desk/web/subscribe.hoon new file mode 100644 index 0000000..b8dac3d --- /dev/null +++ b/desk/web/subscribe.hoon @@ -0,0 +1,131 @@ +|_ who=@p +++ script ^~ %- trip +''' +console.log('oh hai') +''' +++ css ^~ %- trip +''' +#login{ + margin-top: 3rem; + padding-bottom: 3rem; + & h1 { + text-align: center; + } + & form{ + margin: auto; + width: 50%; + text-align: center; + + & input[type=text]{ + outline: none; + padding: 0.5rem; + } + & button{ + display: block; + margin: 1rem auto; + } + + } +} +''' +++ $ ^- manx + :: =/ redirect=(unit @t) (get-header:http 'redirect' u.parsed) +:: =/ redirect-str ?~(redirect-url "" (trip u.redirect-url)) + =/ redirect-str "" +;div#login.blog + ;style: {css} + ;h1: Subscribe + ;p: Some content in this website is only accessible to subscribers. As for why, keep reading on. + ;p + ;span: To subscribe just send me an Urbit DM to ~docteg-mothep, or an email to + ;a/"mailto:sub@spandrell.ch":"sub@spandrell.ch" + == + ;p: There's three kinds of subscriptions: + ;ol + ;li + ;p: Base Subscription: $5/month or $50/year. + == + ;li + ;p: Urbit Subscription: $10/month or $100/year. I'll give you a free Urbit planet and manage it for you. + == + ;li + ;p: Full Subscription: $20/month or $200/year. All the above, early access to my upcoming books and an encrypted @spandrell.ch mailbox. + == + == + ;p: I'll take any mainstream crypto, but can't take cards. + ;p:"Be reminded that I don't keep user data in this website; all logins work through Urbit, so you will need to run an Urbit instance to subscribe anyway" + ;p: Urbit IDs are permanent identities and some people might not want to use their regular planet to post here, in that case I recommend acquiring an Urbit subscription, that way you can have a second Urbit planet for shitposting purposes. + ;h2.tc: Subscriber privileges + ;p: Subscribing will give you the following privileges: + ;ul + ;li + ;p: Ability to comment on all blog posts, past and present. + == + ;li + ;p: Access to all boards in the Forum, including VIP. + == + ;li + ;p: Urbit or Email notifications for threads, blog posts or blog comment threads that you follow. + == + ;li + ;p: Ability to post anonymously or generate new identities. + == + ;li + ;p: Access to the VIP chat rooms. + == + ;li + ;p: Ability to Edit pages in the Wiki. + == + ;li + ;p: Access to the Feed page, where the community puts feeds together with Twitter and RSS content from across the web. + == + ;li + ;p: Access to the Search page, where all blog posts, comments, forum posts and chat messages are searchable. + == + == + ;h2.tc: Grift Rationale + ;p: Many friends have asked me since years ago why I don't start a Substack, or get a bluecheck on Twitter and start grifting there. + ;p:"There's many reasons for that; one being that I just dislike depending on web platforms that can kick me out any day without recourse. It's the current year, we can and should try to handle publishing and payments without needing to ask permission to boomers who if not hate us don't give a shit about us." + ;p: Most importantly, all web2 platforms require KYC. I'd have to doxx myself to Substack, Twitter or whatever. Which isn't that big of a deal these days; it seems the worst of Left-led cancel culture are over. And for all I know the people at Substack like my blog. Still, I'm not Curtis Yarvin, I don't know these people personally and have no reason to trust them. + ;p: I do have bills to pay though, and would like to be have more time to write here and provide a good service for my audience, both with writing and good community software. I firmly believe the internet needs a place where you can say nigger and I want to help. The website is still under construction, it's not fully featured yet, but we'll get there. + +== +++ nudge + |= wat=tape + =/ css %- trip 'a{margin-right: 1ch;}' + ;div.blog.tc + ;style: {css} + ;p:"{wat} page is only available to subscribers" + ;p + ;a/"/subscribe":"Click here" + ;span:"to subscribe to Bloody Shovel 4 and enjoy full access to all content and communities" + == + == +:: ++ new-session-key +:: |- ^- @uv +:: =/ candidate=@uv (~(raw og (shas %session-key eny)) 128) +:: ?. (~(has by sessions.auth.state) candidate) +:: candidate +:: $(eny (shas %try-again candidate)) +:: :: are there cookies passed with this request? +:: =/ cookie-header=@t +:: %+ roll header-list.request +:: |= [[key=@t value=@t] c=@t] +:: ?. =(key 'cookie') +:: c +:: (cat 3 (cat 3 c ?~(c 0 '; ')) value) +:: :: is there an urbauth cookie? +:: :: +:: ?~ urbauth=(get-header:http (crip "urbauth-{(scow %p our)}") u.cookies) +:: ++ session-cookie-string +:: |= [session=@uv extend=?] +:: ^- @t +:: %- crip +:: =; max-age=tape +:: "urbauth-{(scow %p our)}={(scow %uv session)}; Path=/; Max-Age={max-age}" +:: %- format-ud-as-integer +:: ?. extend 0 +:: (div (msec:milly session-timeout) 1.000) +:: ++ cookie-stuff +:: ['set-cookie' (session-cookie-string session-id |)] +-- diff --git a/desk/web/test.udon b/desk/web/test.udon new file mode 100644 index 0000000..0770e90 --- /dev/null +++ b/desk/web/test.udon @@ -0,0 +1,57 @@ +;> + +# H1 + +## H2 + +### H3 + +#### H4 + +##### H5 + +###### H6 + +This is a paragraph with _italics_, *bold* and +`inline code`. Sentences can be hard wrapped. + + +- unordered +- list + ++ ordered ++ list + +[link](https://urbit.org) + + + +``` +fenced codeblock +(note language spec not supported) +``` + +horizontal rule: +--- + +> block quotes + may be hard-wrapped if indented + +Backslash at end\ +of line adds linebreak + +Udon syntax may be prefixed with \*backslashes\* to escape. + +Hoon atom literals like ~sampel-palnet and ~.foo will +be rendered as inline code. + +;table + ;tr + ;td: Arbitrary + ;td: Sail + == + ;tr + ;td: is + ;td: allowed + == +== diff --git a/desk/web/thread.hoon b/desk/web/thread.hoon new file mode 100644 index 0000000..e88185d --- /dev/null +++ b/desk/web/thread.hoon @@ -0,0 +1,478 @@ +/- boke, tp=trill-post, cons=contact +/+ sr=sortug, plib=trill-utils, sigil=sigil-sigil, lib=boke, kaji, constants +/= post-content /web/components/post-text +|_ [fn=full-node:tp children=(list post:tp) page=@ud contacts=whoms:cons =bowl:gall] +++ thread-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{ + & img{ + width: 20px; + margin-left: 1rem; + } + } + + & p{ + font-size: 1.1rem; + } + & .reply{ + display: grid; + border: 1px solid silver; + + & .author{ + margin-right: 1rem; + + & .avatar{ + max-height: 100px; + max-width: 88px; + } + & .name{ + font-weight: 500; + color: var(--hong); + text-align: center; + word-break: break-word; + } + } + + & .metadata { + & p{ + margin: -5px 0 0 0; + font-size: 14px; + } + & .buttons{ + display: flex; + img{ + height: 20px; + width: 20px; + margin: 5px 5px; + cursor: pointer; + } + } + + } + + & .content{ + text-align: left: + word-wrap: break-word; + + & 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%; + } + } + } + & footer{ + width: 100%; + .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; + } + + & #posts{ + + & .reply{ + padding: 1rem; + 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; + } + } + } + } + } + /* mobile */ + @media(max-width: 800px){ + + + & #posts{ + display: block; + width: 100%; + + & .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; + } + } + & .metadata{ + grid-column-start: 2; + grid-column-end: 3; + grid-row-start: 1; + grid-row-end: 2; + padding: 0; + border-bottom: 1px solid grey; + + & .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; + + } + } + } + } + + + + + & #sidebar{ + padding-top: 2rem 0.5rem 0.5rem 0.5rem; + + & div{ + margin: 1rem 0; + } + + & #search-box{ + margin-top: 0.25rem; + + & input{ + width: 100%!important; + border: 1px solid var(--text-color); + } + } + } +} + +.reply-box{ + & .replying-to{ + display: flex; + + & .name{ + margin-left: 1rem; + } + } + & textarea{ + resize: none; + display: block; + outline: none; + width: 100%; + } + & input[type=submit]{ + margin-top: 0.5rem; + float:right; + } +} +''' +++ thread-js ^~ %- trip +''' +const buttons = document.querySelectorAll(".buttons"); +console.log(buttons, "buttons") +buttons.forEach(el => { + const parent = el.closest(".reply"); + const cb = el.querySelector(".copy-button") + cb.addEventListener("click", () => { + const path = `https://spandrell.ch/board/p/${parent.id}` + navigator.clipboard.writeText(path).then(res => { + console.log(res, "write to clipboard") + }).catch(er => { + console.log(er, "couldn't write to clipboard") + }) + }) + const db = el.querySelector(".down-button") + db.addEventListener("click", () => { + parent.querySelector("footer").scrollIntoView(); + }) +}); + +''' +++ $ ^- manx +=/ index (mul (dec page) board-page-size:constants) +=/ childlist (slice:sr children board-page-size:constants index) +;div#board-thread + ;style:"{thread-css}" + ;header.fxc + ;h1:"{(trip title.p.fn)}" + ;+ %+ hide-if:kaji ?|((is-admin:lib src.bowl) .=(author.p.fn src.bowl)) + edit-button + == + ;div#posts + ;div#replies + ;* %+ mapi:sr childlist thread-child + == + ;+ sidebar + == + ;script:"{thread-js}" +== +++ edit-button +=/ pid-string (enc:kaji [author.p.fn id.p.fn]) +;a/"/board/edit/{pid-string}" + ;img@"https://s3.spandrell.ch/assets/board/ui/edit.svg"; +== + +++ thread-child +|= [post-index=@ud =post:tp] ^- manx +=/ base-index (mul (dec page) board-page-size:constants) +=/ pind (add base-index +(post-index)) +=/ usr (user author.post) +=/ pid-string (enc:kaji [author.post id.post]) +=/ jammed-parents %- enc:kaji [op=[author.p.fn id.p.fn] parent=[author.post id.post]] +;div.reply(id pid-string) + ;div.author + ;+ avatar.usr + ;+ name.usr + == + ;div.metadata + ;+ (date post pind) + ;+ (buttons post) + == + ;div.content + ;* (post-content contents.post) + ;+ (footer post) + == + ;+ (reply-box name.usr jammed-parents) +== +++ user +|= u=@p ^- [avatar=manx name=manx] +=/ ming (get-name:lib u) +=/ sig sigil(size 80) +=/ sigl ?: ?=(%ship -.ming) (sig u) +=/ random (random-avatar:lib (jam u)) +;img.avatar@"{random}"; +=/ nam ;div.name:"{ming}" +=/ prof (~(get by contacts) [%.y u]) +?~ prof [sigl nam] +=/ avatar + =/ avatar-data (~(get by info.u.prof) %avatar) + ?~ avatar-data sigl + ?. ?=(%look -.u.avatar-data) sigl + ;img.avatar@"{(trip +.u.avatar-data)}"; +=/ name + =/ name-data (~(get by info.u.prof) %nickname) + ?~ name-data nam + ?. ?=(%text -.u.name-data) nam + ;div.name:"{(trip +.u.name-data)}" + +[avatar name] +++ date +|= [=post:tp post-index=@ud] +=/ date-string (datetime-to-tape:string:sr id.post "-") +=/ posted ;span.posted-date:"{date-string}" +=/ last-edit=time key:head:(pop:corm:tp contents.post) +=/ =marl ?: .=(last-edit id.post) posted^~ +:~ posted + ;span.updated-date:"{(datetime-to-tape:string:sr last-edit "-")}" +== +;div.date + ;p + ;* marl + == + ;p + ;span:"Post #{(scow:parsing:sr %ud post-index)}" + ;span.engagement:"" + == +== +++ buttons +|= =post:tp +:: =/ votes %+ roll ~(tap by reacts.engagement.post) |= [[s=@p [r=@t *]] acc=@] +:: ?: ?|(=(r "thumbsup") =(r "👍")) +;div.buttons + ;+ %+ hide-if:kaji ?|((is-admin:lib src.bowl) .=(author.post src.bowl)) + ;img@"https://s3.spandrell.ch/assets/board/ui/edit.svg"; + ;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 +|= [author=manx jammed-parents=tape] ^- manx +:: %- serve-modal:kaji :_ ~ +;div.reply-box + =hidden "" + ;div.replying-to + ;div:"Replying to" + ;+ author + == + ;form + =kaji "poke" + =action "add-post" + ;textarea + =name "text" + =rows "10" + ; + == + ;input(type "hidden", name "parents", value jammed-parents); + ;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 + ?. .=(i (dec count)) + ;a/"/tags/{(trip t)}":"{(trip t)}, " + ;a/"/tags/{(trip t)}":"{(trip t)} " +;div.tags.flex + ; Tags: + ;* tag-links +== +;footer + ;+ tags +== +:: +++ 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 children)) +=/ 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 + ;button:"Back to Board" + ;div#search-box + ;form.flex + ;input(name "thread-search", placeholder "Search on Thread"); + ;button + ;img@"https://s3.spandrell.ch/assets/board/ui/search.svg"; + == + == + == + ;div#stats + ;p:"{post-count} Posts - {page-count} Pages" + ;p:"Showing posts {froms} to {tos}" + == + ;div#page-nav + ;a.button/"1":"1" + ;a.button/"{prev-page}" + ;img@"https://s3.spandrell.ch/assets/board/ui/left.svg"; + == + ;a.button:"{curr-page}" + ;a.button/"{next-page}" + ;img@"https://s3.spandrell.ch/assets/board/ui/right.svg"; + == + ;a.button/"{page-count}":"{page-count}" + == + ;button + =kaji "toggle" + =targ ".reply-box" + =modal "1" + ; Reply + == +== +-- diff --git a/desk/web/tv/router.hoon b/desk/web/tv/router.hoon new file mode 100644 index 0000000..6f4df21 --- /dev/null +++ b/desk/web/tv/router.hoon @@ -0,0 +1,39 @@ +/- boke, tp=trill-post, cnt=contact +/+ kaji, fetch-lib=fetch, plib=trill-utils, const=constants, sr=sortug, lib=boke +/= index /web/index +/= tvp /web/tv/tv +/= subscribe /web/subscribe +|_ [rl=req-line:kaji s=state:boke =bowl:gall] ++* fetch ~(. fetch-lib [s bowl]) + tv ~(. tvp [s bowl]) +++ eyre-bail (error-response:kaji 404) +++ manx-bail (error-page:kaji 404) +:: +++ $ ^- eyre-res:kaji + =/ p pat.rl ::?. mob.rl pat.rl [%m pat.rl] + :: ?. (is-subscribed:lib src.bowl) nudge + ?+ p eyre-bail + ~ main + [%f *] fragment + == +++ main + :- %page + =/ ut (~(get by here.tv.s) 'spandrell-tv') + ?~ ut manx-bail + =/ sta (~(get by schedule.u.ut) started.current.u.ut) + ?~ sta manx-bail :: TODO old station sorry + =/ page (main:tv ['spandrell-tv' %our 'Spandrell TV' ~docteg-mothep u.sta]) + (index ~[page] bowl) + +++ nudge + :- %page + =/ sub ~(. subscribe src.bowl) + =/ nudgep (nudge:sub "TV") + (index ~[nudgep] bowl) +++ fragment + :- %html + ?+ pat.rl manx-bail + [%f %s ~] ;div ;* tv-list:tv == + [%f %u ~] ;div ;* radio-list:tv == + == +-- diff --git a/desk/web/tv/tv.hoon b/desk/web/tv/tv.hoon new file mode 100644 index 0000000..5a3360a --- /dev/null +++ b/desk/web/tv/tv.hoon @@ -0,0 +1,285 @@ +/- *boke +/+ kaji, sr=sortug, lib=boke +/= post-text /web/components/post-text +|_ [s=state =bowl:gall] +++ css ^~ %- trip +''' +#tv{ + padding: 1rem; + + & main{ + margin-top: 0.5rem; + height: 50vw; + & #screen{ + } + & #chat{ + border: 1px solid var(--text-color); + h3{ + height: 1.5rem; + margin: 0; + } + + & #chat-container{ + overflow-y: scroll; + max-height: calc(100% - 3.5rem); + height: calc(100% - 3.5rem); + .chat-msg{ + padding: 0.5rem; + & .author{ + opacity: 0.8; + font-family: Inter; + } + & .chat-content{ + padding: 0.3rem 1rem; + & p{ + margin: 0; + } + } + } + } + & form{ + display: flex; + margin-bottom: 0; + border-top: 1px solid black; + + & input{ + border: none; + border-radius: 0; + } + + & input[type=text]{ + width: 100%; + height: 2rem; + } + & input[type=submit]{ + padding: 0.2rem; + } + } + } + } + & #radio-list{ + overflow-y: scroll; + max-height: 70vh; + & .entry{ + cursor: pointer; + } + } +} +#video{ + width: 100%; + background-color: black; +} +.tabs{ + display: flex; + gap: 1rem; + & .tab{ + cursor: pointer; + } +} +@media (min-width: 1200px){ +#tv main{ + display: flex; + align-items: stretch; + gap: 1rem; + +} +#screen{ + width: 75%; +} +#chat{ + width: 25%; +} +@media (max-width: 1200px){ +#screen{ + width: 100%; +} +#chat{ + width: 25%; +} + +} +.movie-name{ + font-size: 1.2rem; + font-weight: 500; +} +''' +++ script +|= sub-path=tape +^~ %+ weld %- trip +''' +function initVideo(videoSrc){ + const video = document.getElementById('video'); + video.autoplay = true; + console.log(videoSrc, "starting video") + + if (Hls.isSupported()) { + var hls = new Hls(); + hls.loadSource(videoSrc); + hls.attachMedia(video); + hls.on(Hls.Events.MANIFEST_LOADED, function (event, data) { + console.log("manifest loaded", [event, data]) + }); + hls.on(Hls.Events.MANIFEST_LOADED, function (event, data) { + console.log("manifest loaded", [event, data]) + }); + hls.on(Hls.Events.FRAG_LOADED, function (event, data) { + console.log("frag loaded", [event, data]) + }); + hls.on(Hls.Events.LEVEL_LOADED, function (event, data) { + console.log("level loaded", [event, data]) + }); + hls.on(Hls.Events.SUBTITLE_TRACK_SWITCH, function (event, data) { + console.log("sub switch", [event, data]) + }); + hls.on(Hls.Events.SUBTITLE_TRACK_LOADED, function (event, data) { + console.log("sub loaded", [event, data]) + }); + hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) { + console.log("manifest parsed", [event, data]) + video.play(); + }); + } + // HLS.js is not supported on platforms that natively support HLS + else if (video.canPlayType('application/vnd.apple.mpegurl')) { + video.src = videoSrc; + video.addEventListener('loadedmetadata', function () { + video.play(); + }); + } + +} +document.addEventListener('DOMContentLoaded', () => { + const src = 'https://hydrogen.finnem.net/hls/live.m3u8'; + //const src = 'https://hls.urbit.cam/output.m3u8'; + initVideo(src); + const room = document.getElementById("chat-container"); + if (room) room.scrollTop = room.scrollHeight +}); + +document.addEventListener('kaji-fact', (event) => { + console.log(event.detail, "kaji fact") + if (event.detail.event === "scroll"){ + const room = document.getElementById("chat-container"); + if (room) setTimeout(() => room.scrollTop = room.scrollHeight, 50) + } + else if ("change-radio" in event.detail) + initVideo(event.detail["change-radio"]); +}); + +''' +""" +subscribe('{sub-path}'); + +""" +++ main +|= [symbol=@tas =twr] ^- manx + =/ author (scow %p owner.twr) + =/ name (trip symbol) + =/ sub-path "/tv/{(trip -.twr)}/{name}" + ~& sub-path=sub-path +;div#tv + ;style: {css} + ;main + ;div#screen + ;video#video + =controls "" + ; + == + ;+ (broadcast-metadata twr) + == + ;+ (chat-box symbol twr) + == + :: ;+ rl + ;script: {(script sub-path)} + ;script@"https://cdn.jsdelivr.net/npm/hls.js@latest"; +== +++ broadcast-metadata +|= =twr ^- manx + =/ author (scow %p owner.twr) + =/ stat-name (trip name.twr) + ?: ?=(%our -.twr) + =/ desc (trip description.tv.twr) + ;div.meta + ;div.movie-name:"{desc}" + ;div.movie-from:"from {stat-name} by {author}" + == + :: + =/ desc (trip description.p.twr) + ;div.meta + ;div:"{desc}" + ;div:"by {author}" + == + +++ rl ^- manx + ;div#radio-list + ;div.tabs + =kaji "scry" + =swap "swap" + =targ "#rlist" + ;div.tab + =path "/tv/f/s" + ; Spandrell TV + == + ;div.tab + =path "/tv/f/u" + ; Urbit Radio + == + == + ;div#rlist + ;* tv-list + == + == +++ radio-list ^- marl + %+ turn ~(tap by urbit.tv.s) |= [p=@p q=urbit-radio] + ;div.entry + =kaji "poke" + =action "change-radio" + =payload (enc:kaji p) + ;p:"{(trip description.q)}" + ;p:"by {(scow %p p)}" + == +++ tv-list ^- marl + %+ turn ~(tap by here.tv.s) |= [p=@t q=bstv] + ;div.entry + =kaji "poke" + =action "change-nradio" + =payload (trip p) + ;p:"{(trip playing.current.q)}" + ;p:"by {(scow %p owner.q)}" + == + +++ chat-box +|= [symbol=@tas =twr] +?: ?=(%urb -.twr) ;div; +;div#chat + ;h3.tc.bb:"Chat" + ;div#chat-container + ;* %+ turn (flop chat.tv.twr) chat-msg + == + ;form + =id "chat-composer" + =kaji "poke" + =action "add-radio-chat" + =wipe "yes" + ;input#text-input(type "text", name "input", placeholder "Nice", autocomplete "off"); + ;input(type "hidden", name "type", value (trip -.twr)); + ;input(type "hidden", name "owner", value (scow %p owner.twr)); + ;input(type "hidden", name "name", value (trip symbol)); + ;input(type "submit", value "⇛"); + == + +== +++ chat-msg +|= p=post:tp +:: =/ usr (user author.p whoms 32) + =/ author ?: (is-anon:lib author.p) "anon" (scow %p author.p) +;div.chat-msg + ;div.f1 + ;div.author:"{author}:" + ;div.time:"{(time-to-tape:string:sr id.p)}" + == + ;div.chat-content + ;* (content:post-text contents.p) + == +== +-- diff --git a/desk/web/user.hoon b/desk/web/user.hoon new file mode 100644 index 0000000..5076f0b --- /dev/null +++ b/desk/web/user.hoon @@ -0,0 +1,71 @@ +/- *boke, tp=trill-post, cnt=contact +/+ sigil=sigil-sigil, sr=sortug, wall +/= user /web/components/user +|_ [who=@p s=state =bowl:gall] +++ css ^~ %- trip +''' +#user{ + margin-top: 1rem; + + & .avatar svg{ + margin: auto; + } + .button{ + display: block; + width: max-content; + margin: auto; + } +} +''' +++ script ^~ %- trip +''' + const el = document.getElementById("logout"); + el.addEventListener("click", run); + async function run(){ + // const res = await fetch("/~/logout?redirect=/"); + const res = await fetch("/~/logout"); + if (res) window.location.href = "/"; + console.log(res, "res") + } +''' ++$ stats [count=@ud first=@da last=@da] +++ get-stats ^- stats + %+ roll (tap:gorm:tp feed.s) |= [[=pid:tp *] a=stats] + ?. .=(ship.pid who) a + =. count.a +(count.a) + =. first.a + ?: .=(first.a *@da) id.pid + ?: (lth id.pid first.a) id.pid first.a + =. last.a ?: (gth id.pid last.a) id.pid last.a + a + +++ $ + =/ wal ~(. wall src.bowl) + =/ contacts (get-contacts:cnt bowl) + =/ usr (user src.bowl contacts 100) + =/ is-self .=(src.bowl who) + =/ stats get-stats + =/ subscription (subscription-type:wal now.bowl) + + +;div#user.blog + ;style: {css} + ;h1.tc: User Profile + ;+ -.usr + ;h2.tc + ;+ +.usr + == + ;+ ?: .=(0 count.stats) ;p:"No posts" + ;div.stats + ;p: Post Count: {(number:string:sr count.stats)} + ;p: First post: {(date-to-tape:string:sr first.stats "/")} + ;p: Latest post: {(date-to-tape:string:sr last.stats "/")} + == + ;* ?. is-self ~ + ;* ?~ subscription + ;+ ;a.button/"/subscribe":"Subscribe" ~ + ;+ ;button#logout.button:"Logout" + ;script:"{script}" + +== +-- |