From 81c4d517735983a5afd6e9dc800257c761598527 Mon Sep 17 00:00:00 2001 From: "Jakob L. Kreuze" Date: Sat, 13 Jul 2024 18:04:05 -0400 Subject: The `org' directory is no longer necessary --- static/css/fonts.css | 59 ++++ static/css/highlight.css | 61 ++++ static/css/normalize.css | 349 ++++++++++++++++++++ static/css/style.css | 557 ++++++++++++++++++++++++++++++++ static/font/Cantarell-Bold.otf | Bin 0 -> 69196 bytes static/font/Cantarell-BoldOblique.otf | Bin 0 -> 75888 bytes static/font/Cantarell-Oblique.otf | Bin 0 -> 74892 bytes static/font/Cantarell-Regular.otf | Bin 0 -> 68340 bytes static/font/Hack-Bold.ttf | Bin 0 -> 317628 bytes static/font/Hack-BoldItalic.ttf | Bin 0 -> 322288 bytes static/font/Hack-Italic.ttf | Bin 0 -> 316156 bytes static/font/Hack-Regular.ttf | Bin 0 -> 309408 bytes static/gpg.txt | 87 +++++ static/image/cbt-2023-04-13.jpg | Bin 0 -> 375364 bytes static/image/cc-by-sa-4.0.png | Bin 0 -> 475 bytes static/image/clock.png | Bin 0 -> 46036 bytes static/image/default-icon.png | Bin 0 -> 545281 bytes static/image/favicon.ico | Bin 0 -> 5430 bytes static/image/hourglass.gif | Bin 0 -> 21999 bytes static/image/i2p.svg | 1 + static/image/lambda.svg | 3 + static/image/lobsters.png | Bin 0 -> 4535 bytes static/image/oneko.gif | Bin 0 -> 3316 bytes static/image/opengraph-icon-coq.jpg | Bin 0 -> 95670 bytes static/image/portrait.png | Bin 0 -> 331269 bytes static/image/profile-picture.jpg | Bin 0 -> 69228 bytes static/image/tango-emblem-important.png | Bin 0 -> 81310 bytes static/image/tor.svg | 42 +++ static/image/webmention-logo.png | Bin 0 -> 5579 bytes static/js/comment-reaction.js | 143 ++++++++ static/js/dynamic-comment-form.js | 94 ++++++ static/js/oneko.js | 182 +++++++++++ static/js/proof-of-work.js | 109 +++++++ static/js/rsvp.js | 206 ++++++++++++ static/js/section-folds.js | 66 ++++ static/js/webmention.js | 229 +++++++++++++ 36 files changed, 2188 insertions(+) create mode 100644 static/css/fonts.css create mode 100644 static/css/highlight.css create mode 100644 static/css/normalize.css create mode 100644 static/css/style.css create mode 100644 static/font/Cantarell-Bold.otf create mode 100644 static/font/Cantarell-BoldOblique.otf create mode 100644 static/font/Cantarell-Oblique.otf create mode 100644 static/font/Cantarell-Regular.otf create mode 100644 static/font/Hack-Bold.ttf create mode 100644 static/font/Hack-BoldItalic.ttf create mode 100644 static/font/Hack-Italic.ttf create mode 100644 static/font/Hack-Regular.ttf create mode 100644 static/gpg.txt create mode 100644 static/image/cbt-2023-04-13.jpg create mode 100644 static/image/cc-by-sa-4.0.png create mode 100644 static/image/clock.png create mode 100644 static/image/default-icon.png create mode 100644 static/image/favicon.ico create mode 100644 static/image/hourglass.gif create mode 100644 static/image/i2p.svg create mode 100644 static/image/lambda.svg create mode 100644 static/image/lobsters.png create mode 100644 static/image/oneko.gif create mode 100644 static/image/opengraph-icon-coq.jpg create mode 100644 static/image/portrait.png create mode 100644 static/image/profile-picture.jpg create mode 100644 static/image/tango-emblem-important.png create mode 100644 static/image/tor.svg create mode 100644 static/image/webmention-logo.png create mode 100644 static/js/comment-reaction.js create mode 100644 static/js/dynamic-comment-form.js create mode 100644 static/js/oneko.js create mode 100644 static/js/proof-of-work.js create mode 100644 static/js/rsvp.js create mode 100644 static/js/section-folds.js create mode 100644 static/js/webmention.js (limited to 'static') diff --git a/static/css/fonts.css b/static/css/fonts.css new file mode 100644 index 0000000..05a70c6 --- /dev/null +++ b/static/css/fonts.css @@ -0,0 +1,59 @@ +/* Cantarell */ + +@font-face { + font-family: 'Cantarell'; + src: url('/static/font/Cantarell-Bold.otf') format('opentype'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Cantarell'; + src: url('/static/font/Cantarell-BoldOblique.otf') format('opentype'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'Cantarell'; + src: url('/static/font/Cantarell-Oblique.otf') format('opentype'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Cantarell'; + src: url('/static/font/Cantarell-Regular.otf') format('opentype'); + font-weight: normal; + font-style: normal; +} + +/* Hack */ + +@font-face { + font-family: 'Hack'; + src: url('/static/font/Hack-Bold.ttf'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('/static/font/Hack-BoldItalic.ttf'); + font-weight: bold; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('/static/font/Hack-Italic.ttf'); + font-weight: normal; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('/static/font/Hack-Regular.ttf'); + font-weight: normal; + font-style: normal; +} diff --git a/static/css/highlight.css b/static/css/highlight.css new file mode 100644 index 0000000..1e8899f --- /dev/null +++ b/static/css/highlight.css @@ -0,0 +1,61 @@ +.org-preprocessor, +.org-builtin { + color: #afd7d7; +} + +.org-comment-delimiter, +.org-comment { + color: #ff8700; +} + +.org-constant { + color: #87ffd7; +} + +.org-function-name { + color: #afd7ff; +} + +.org-keyword { + color: #1fb6d3; +} + +.org-negation-char { + color: #ff4b4b; +} + +.org-regexp-grouping-backslash { + color: #ffd787; +} + +.org-regexp-grouping-construct { + color: #d18aff; +} + +.org-doc, +.org-doc-string, +.org-string { + color: #ffaf87; +} + +.org-type { + color: #87ff87; +} + +.org-variable-name { + color: #ffd787; +} + +.org-warning { + color: #dd0000; +} + + +span.org-diff-added { text-decoration: none; background: #c0f8d0; font-family: "Inconsolata"; font-stretch: semi-condensed; font-weight: 600; font-style: normal; color: #000000; font-size: 10pt; } +span.org-diff-added a { background: #c0f8d0; font-family: "Inconsolata"; font-stretch: semi-condensed; font-weight: 600; font-style: normal; color: #000000; font-size: 10pt; text-decoration: underline; } +span.org-diff-indicator-added { text-decoration: none; background: #c0f8d0; font-weight: 700; font-family: "Inconsolata"; font-stretch: semi-condensed; font-style: normal; color: #000000; font-size: 10pt; } +span.org-diff-indicator-added a { background: #c0f8d0; font-weight: 700; font-family: "Inconsolata"; font-stretch: semi-condensed; font-style: normal; color: #000000; font-size: 10pt; text-decoration: underline; } +span.org-diff-removed { text-decoration: none; background: #ffd8d5; font-family: "Inconsolata"; font-stretch: semi-condensed; font-weight: 600; font-style: normal; color: #000000; font-size: 10pt; } +span.org-diff-removed a { background: #ffd8d5; font-family: "Inconsolata"; font-stretch: semi-condensed; font-weight: 600; font-style: normal; color: #000000; font-size: 10pt; text-decoration: underline; } +span.org-diff-indicator-removed { text-decoration: none; background: #ffd8d5; font-weight: 700; font-family: "Inconsolata"; font-stretch: semi-condensed; font-style: normal; color: #000000; font-size: 10pt; } +span.org-diff-indicator-removed a { background: #ffd8d5; font-weight: 700; font-family: "Inconsolata"; font-stretch: semi-condensed; font-style: normal; color: #000000; font-size: 10pt; text-decoration: underline; } diff --git a/static/css/normalize.css b/static/css/normalize.css new file mode 100644 index 0000000..192eb9c --- /dev/null +++ b/static/css/normalize.css @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Render the `main` element consistently in IE. + */ + +main { + display: block; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..52d19af --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,557 @@ +[hidden] { display: none !important; } + +html { + font-family: "Cantarell", sans-serif; +} + +@media (max-width: 54em) { + html { + font-size: 14px; + line-height: 160%; + } +} + +@media (max-width: 26em) { + html { + font-size: 12px; + line-height: 150%; + } +} + +body { + max-width: 54rem; + margin: .5rem auto; + position: relative; +} + +@media (max-width: 54em) { + body { + max-width: 100%; + padding: 0 1rem; + } +} + +/* Top-level header. */ + +body > header { + display: flex; + align-items: center; +} + +@media screen and (max-width: 360px) { + body > header { + flex-direction: column; + flex-wrap: wrap; + justify-content: space-around; + } +} + +body > header > a > img { + max-width: 4rem; +} + +@media screen and (min-width: 480px) { + body > header > a { + padding-right: 1rem; + } +} + +body > header > nav ul { + list-style-type: none; + padding-left: 0; +} + +@media screen and (min-width: 480px) { + body > header > nav > ul > li { + display: inline-block; + } + + body > header > nav > ul li { + padding: 1rem 2rem; + border-bottom: 2px solid #d2d6dd; + text-align: center; + } + + body > header > nav > ul li > a { + color: #000; + text-decoration: none; + } + + body > header > nav > ul .drop-menu { + display: none; + position: absolute; + z-index: 1; + } + + body > header > nav > ul .drop-menu > li { + background: #fff; + } + + body > header > nav > ul > li:hover .drop-menu { + display: block; + } +} + +@media screen and (max-width: 480px) { + body > header > nav > ul .drop-menu { + list-style-type: circle; + padding-left: 1em; + } +} + +/* Horizontal lists. */ + +.horizontal-list { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + + list-style-type: none; + text-align: center; + + margin: 0; + padding: 8px 0; +} + +/* Pagination. */ + +#pagination { + display: flex; + justify-content: space-around; +} + +/* Tag clouds. */ + +#tag-cloud a { + color: #000; +} + +#tag-cloud li { + display: inline-block; + margin: 10px 5px; + padding: 5px 10px; + vertical-align: top; + + text-align: center; + + border-radius: 4px; + border-style: solid; + border-width: thin; +} + +/* Links. */ + +a:link, a:visited { color: #00E; text-decoration: none; } + +/* Figures */ + +figure > img { + display: block; + max-height: 600px; + max-width: 90%; + margin: auto; + + border-radius: 2px; +} + +figure > .org-svg { + display: block; + max-width: 90%; + margin: auto; +} + +figure > figcaption { + padding-top: 1rem; +} + +/* Embeds. */ + +.mastodon > iframe, +.peertube > iframe { + display: block; + width: 90%; + min-height: 100%; + margin: auto; + border-radius: 4px; +} + +/* Footnotes. */ + +.footdef { + margin-bottom: 1rem; +} + +.footdef > sup, .footpara { + display: inline; +} + +/* Comments & Webmention. */ + +textarea { + width: 100%; + height: 4rem; +} + +textarea, input[type=text], input[type=url] { + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); +} + +#webmention p .author-name { + margin: 4px; +} + +#webmention { + border-top: 2px solid #d2d6dd; +} + +ul.webmention-container { + list-style-type: none; +} + +ul.webmention-container > li { + border: 2px solid #d2d6dd; + border-bottom: none; +} + +ul.webmention-container > li:last-child { + border-bottom: 2px solid #d2d6dd; +} + +ul.webmention-container > li.comment { + padding: 0.5rem; +} + +ul.webmention-container > li.comment img.u-photo { + width: 4rem; + padding: 0 1rem 1rem 0; + float: left; + clear: both; + height: 100%; +} + +ul.webmention-container > li.interaction { + padding: 4px; +} + +ul.webmention-container > li.interaction img.u-photo { + max-width: 16px; + margin-right: 8px; + float: left; + clear: both; +} + +ul.webmention-container ul.comment-reactions { + margin-top: 8px; +} + +ul.webmention-container ul.comment-reactions li { + border-radius: 4px; + border: 1px solid #d2d6dd; + display: inline; + list-style-type: none; + padding: 4px; +} + +/* Webmention (or comment) source indicator. */ + +ul.webmention-container .comment { + position: relative; +} + +ul.webmention-container .comment .comment-source-identifier { + position: absolute; + top: 4px; + right: 4px; + max-width: 16px; +} + +/* Widget for comment reactions. */ + +.emoji-picker { + width: 51%; + height: 2.75em; + border: solid 1px; + overflow: scroll; + margin-top: 16px; +} + +.emoji-picker > span { + margin: 2px; + font-size: 2em; +} + + +/* Webmention form. */ + +form#webmention-form { + border-top: 2px solid #d2d6dd; + padding-top: 1rem; +} + +#webmention-input-group { + margin-top: 1rem; + display: grid; + grid-template-columns: 9fr 1fr; +} + +/* Comment form. */ + +.required:after { + content:" *"; + color: red; +} + +fieldset#comment-content { + display: grid; + grid-template-columns: 1fr 2fr; + grid-gap:5px; +} + +#comment-form #captcha-trigger-block label { + display: block; + margin-bottom: 16px; +} + +#comment-form img { + max-width: 100%; +} + +/* Source code. */ + +pre { + color: #f0e9dc; + font-family: "Hack", monospace; + font-size: 90%; + + background-color: #303539; + border-radius: 4px; + padding: 1rem; + overflow-x: auto; +} + +/* Footer. */ + +body > footer { + margin-top: 1.2rem; + border-top: 2px solid #d2d6dd; +} + +body > footer > div { + margin-top: 1rem; + display: flex; + align-items: center; +} + +body > footer > div > p { + margin: 0; + padding-right: 1rem; +} + +/* About page. */ + +.overline { + border-top: 2px solid #d2d6dd; + margin-top: 0.5rem; + overflow: auto; /* Not specific to overline. */ +} + +/* Tables. */ + +table { + text-align: center; + width: 100%; +} + +td { + padding: 0 1rem; +} + +tr:nth-child(even) { + background-color: #f2f2f2; +} + +/* Buttons. */ + +.lang button { + display: inline; + padding: .3em .6em .3em; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.label-info { + background-color: #5bc0de; +} + +.label-good { + background-color: #5cb85c; + border-color: #4cae4c; +} + +.label-okay { + background-color: #ec971f; + border-color: #d58512; +} + +.label-bad { + background-color: #dc3545; +} + +/* Spoilered text. */ + +.spoiler { + background-color: #2e3436; + color: #2e3436; +} + +.spoiler:hover { + background-color: #f6f5f4; +} + +/* Folded sections. */ +.fold-hidden { + height: 300px; + overflow: hidden; + mask-image: linear-gradient(to bottom, black 0%, black 80%, transparent 100%); +} + +/* Styles specific to the "about" page. */ + +.portrait { + max-width: 30%; + float: right; +} + +.portrait > img { + max-width: 100%; +} + +.project-section td:nth-child(1) { + width: 15%; +} + +.project-section td:nth-child(2) { + width: 10%; +} + +.project-section td:nth-child(3) { + width: 80px; +} + +.project-section td:nth-child(4) { + width: 15%; +} + +.team-timeline-section img { + max-width: 32px; +} + +.team-timeline-section td:nth-child(1) { + width: 15%; +} + +.team-timeline-section td:nth-child(2) { + width: 40px; +} + +/* .team-timeline-section td:nth-child(3) { */ +/* width: 20%; */ +/* } */ + +.team-timeline-section td:nth-child(4) { + width: 15%; +} + +.team-timeline-section td:nth-child(5) { + width: 15%; +} + +.lang-c { + background-color: #555555; + color: #fff; +} + +.lang-cpp { + background-color: #f34b7d; + color: #fff; +} + +.lang-python { + background-color: #3572a5; + color: #ffdb4e; +} + +.lang-rust { + background-color: #dea584; + color: #fff; +} + +.lang-kotlin { + background-color: #f89917; + color: #fff; +} + +.lang-fennel { + background-color: #2ca02c; + color: #fff; +} + +.lang-elisp { + background-color: #c065db; + color: #fff; +} + +.lang-scheme { + background-color: #1e4aec; + color: #fff; +} + +.lang-ebuild { + background-color: #4d4270; + color: #fff; +} + +.article-front-matter { + border: 1px solid; + border-radius: 3px; + padding: 0.25em 1em; +} + +.article-warning { + background-image: url('/static/image/tango-emblem-important.png'); + background-repeat: no-repeat; + background-size: 4em 4em; + background-position: 1.5em 50%; + padding: 0.25em 1em 0.25em 7em; +} + +.article-future-interjection { + background-image: url('/static/image/clock.png'); + background-repeat: no-repeat; + background-size: 4em 4em; + background-position: 1.5em 50%; + padding: 0.25em 1em 0.25em 7em; +} + +#loading { + border: 1px solid; + width: fit-content; + padding: 5px; + margin: auto; +} + +fieldset { + padding: 12px; + margin: 15px; +} + +#rsvp-input input { + margin: 0px 16px; +} diff --git a/static/font/Cantarell-Bold.otf b/static/font/Cantarell-Bold.otf new file mode 100644 index 0000000..a5b620b Binary files /dev/null and b/static/font/Cantarell-Bold.otf differ diff --git a/static/font/Cantarell-BoldOblique.otf b/static/font/Cantarell-BoldOblique.otf new file mode 100644 index 0000000..5f49d64 Binary files /dev/null and b/static/font/Cantarell-BoldOblique.otf differ diff --git a/static/font/Cantarell-Oblique.otf b/static/font/Cantarell-Oblique.otf new file mode 100644 index 0000000..70539ed Binary files /dev/null and b/static/font/Cantarell-Oblique.otf differ diff --git a/static/font/Cantarell-Regular.otf b/static/font/Cantarell-Regular.otf new file mode 100644 index 0000000..fca6759 Binary files /dev/null and b/static/font/Cantarell-Regular.otf differ diff --git a/static/font/Hack-Bold.ttf b/static/font/Hack-Bold.ttf new file mode 100644 index 0000000..7ff4975 Binary files /dev/null and b/static/font/Hack-Bold.ttf differ diff --git a/static/font/Hack-BoldItalic.ttf b/static/font/Hack-BoldItalic.ttf new file mode 100644 index 0000000..3b137d9 Binary files /dev/null and b/static/font/Hack-BoldItalic.ttf differ diff --git a/static/font/Hack-Italic.ttf b/static/font/Hack-Italic.ttf new file mode 100644 index 0000000..d26198a Binary files /dev/null and b/static/font/Hack-Italic.ttf differ diff --git a/static/font/Hack-Regular.ttf b/static/font/Hack-Regular.ttf new file mode 100644 index 0000000..92a90cb Binary files /dev/null and b/static/font/Hack-Regular.ttf differ diff --git a/static/gpg.txt b/static/gpg.txt new file mode 100644 index 0000000..1de4e2a --- /dev/null +++ b/static/gpg.txt @@ -0,0 +1,87 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +tSuc5igEcOo6 +=dfc8 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/static/image/cbt-2023-04-13.jpg b/static/image/cbt-2023-04-13.jpg new file mode 100644 index 0000000..f691e45 Binary files /dev/null and b/static/image/cbt-2023-04-13.jpg differ diff --git a/static/image/cc-by-sa-4.0.png b/static/image/cc-by-sa-4.0.png new file mode 100644 index 0000000..003dd0a Binary files /dev/null and b/static/image/cc-by-sa-4.0.png differ diff --git a/static/image/clock.png b/static/image/clock.png new file mode 100644 index 0000000..d35c884 Binary files /dev/null and b/static/image/clock.png differ diff --git a/static/image/default-icon.png b/static/image/default-icon.png new file mode 100644 index 0000000..832f3ef Binary files /dev/null and b/static/image/default-icon.png differ diff --git a/static/image/favicon.ico b/static/image/favicon.ico new file mode 100644 index 0000000..842478f Binary files /dev/null and b/static/image/favicon.ico differ diff --git a/static/image/hourglass.gif b/static/image/hourglass.gif new file mode 100644 index 0000000..e1bf222 Binary files /dev/null and b/static/image/hourglass.gif differ diff --git a/static/image/i2p.svg b/static/image/i2p.svg new file mode 100644 index 0000000..dfc85a8 --- /dev/null +++ b/static/image/i2p.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/image/lambda.svg b/static/image/lambda.svg new file mode 100644 index 0000000..76c26c2 --- /dev/null +++ b/static/image/lambda.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/image/lobsters.png b/static/image/lobsters.png new file mode 100644 index 0000000..e75cc99 Binary files /dev/null and b/static/image/lobsters.png differ diff --git a/static/image/oneko.gif b/static/image/oneko.gif new file mode 100644 index 0000000..a009c2c Binary files /dev/null and b/static/image/oneko.gif differ diff --git a/static/image/opengraph-icon-coq.jpg b/static/image/opengraph-icon-coq.jpg new file mode 100644 index 0000000..33c969a Binary files /dev/null and b/static/image/opengraph-icon-coq.jpg differ diff --git a/static/image/portrait.png b/static/image/portrait.png new file mode 100644 index 0000000..dfaee55 Binary files /dev/null and b/static/image/portrait.png differ diff --git a/static/image/profile-picture.jpg b/static/image/profile-picture.jpg new file mode 100644 index 0000000..cd2e07e Binary files /dev/null and b/static/image/profile-picture.jpg differ diff --git a/static/image/tango-emblem-important.png b/static/image/tango-emblem-important.png new file mode 100644 index 0000000..81c455b Binary files /dev/null and b/static/image/tango-emblem-important.png differ diff --git a/static/image/tor.svg b/static/image/tor.svg new file mode 100644 index 0000000..ff0f894 --- /dev/null +++ b/static/image/tor.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/image/webmention-logo.png b/static/image/webmention-logo.png new file mode 100644 index 0000000..a4ffda1 Binary files /dev/null and b/static/image/webmention-logo.png differ diff --git a/static/js/comment-reaction.js b/static/js/comment-reaction.js new file mode 100644 index 0000000..1002fa2 --- /dev/null +++ b/static/js/comment-reaction.js @@ -0,0 +1,143 @@ +/* + * comment-reaction.js -- Simple emoji picker and glue for reaction endpoint + * Copyright © 2023 Jakob L. Kreuze + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + */ + +function encodeAsFormData(obj) { + // Turn the data object into an array of URL-encoded key/value pairs. + let urlEncodedDataPairs = []; + for (let key in obj) { + urlEncodedDataPairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])); + } + return urlEncodedDataPairs.join('&'); +} + +function postReaction(reaction, callback) { + let xhr = new XMLHttpRequest(); + xhr.open('POST', "/api/comment/react", true); + + // Endpoint expects HTML form data. + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + + xhr.onload = function(data) { + switch (xhr.status) { + case 200: + callback(); + break; + case 429: + alert("Failed -- you can react at most once an hour."); + break; + default: + let resp = JSON.parse(xhr.response); + alert("Failed -- " + resp.error); + break; + } + } + xhr.onerror = function(error) { + throw new Error(`Request failed: ${error}`); + } + + xhr.send(encodeAsFormData(reaction)); +} + +function selectorWidgetName(id) { + return `emoji-picker-${id}`; +} + +// Simple Emoji picker widget. +function selectorWidget(id) { + function emojiButton(codepoint) { + let span = document.createElement("span"); + span.innerHTML = String.fromCodePoint(codepoint); + return span; + } + + let selector = document.createElement("div"); + selector.setAttribute("id", selectorWidgetName(id)); + selector.setAttribute("class", "emoji-picker"); + + for (let i = 0x1F600; i < 0x1F64F; i++) { + selector.appendChild(emojiButton(i)); + } + selector.appendChild(emojiButton(0x1F37B)); + selector.appendChild(emojiButton(0x1F440)); + selector.appendChild(emojiButton(0x1F446)); + selector.appendChild(emojiButton(0x1F447)); + selector.appendChild(emojiButton(0x1F448)); + selector.appendChild(emojiButton(0x1F449)); + selector.appendChild(emojiButton(0x1F450)); + selector.appendChild(emojiButton(0x270A)); + selector.appendChild(emojiButton(0x270B)); + selector.appendChild(emojiButton(0x270C)); + selector.appendChild(emojiButton(0x270D)); + for (let i = 0x1F90C; i < 0x1F9FF; i++) { + selector.appendChild(emojiButton(i)); + } + return selector; +} + +// Add a "react" button to every local comment. +window.addEventListener("load", () => { + // First pass to add a separator for the buttons. + let replyButtons = document.querySelectorAll(".comment-reply-button"); + for (let button of replyButtons) { + let parent = button.parentNode; + parent.innerHTML += " - "; + + } + + // Second pass to add the actual "react" button. + replyButtons = document.querySelectorAll(".comment-reply-button"); + for (let button of replyButtons) { + let id = button.getAttribute("data-reply-to-id"); + let reactButton = document.createElement("a"); + reactButton.setAttribute("class", "comment-react-button"); + reactButton.setAttribute("href", "#"); + reactButton.setAttribute("data-react-to-id", id); + reactButton.innerHTML = "react"; + button.parentNode.appendChild(reactButton); + + reactButton.addEventListener('click', (e) => { + // If we already made a selector widget for this comment, let's "hide" the + // one that exists. + let existingSelectorWidget = document.getElementById(selectorWidgetName(id)); + if (existingSelectorWidget !== null) { + existingSelectorWidget.remove(); + e.preventDefault(); + return; + } + + let emojiPicker = selectorWidget(id); + button.parentNode.parentNode.insertBefore(emojiPicker, button.parentNode); + for (let button of emojiPicker.children) { + button.addEventListener('click', (e) => { + postReaction({ + "id": reactButton.getAttribute("data-react-to-id"), + "reaction": button.innerHTML + }, () => { + let reactAcknowledgement = document.createElement("span"); + reactAcknowledgement.innerHTML = "successfully reacted!" + reactButton.replaceWith(reactAcknowledgement); + }); + emojiPicker.remove(); + }); + } + + e.preventDefault(); + }); + } +}); diff --git a/static/js/dynamic-comment-form.js b/static/js/dynamic-comment-form.js new file mode 100644 index 0000000..4be2d52 --- /dev/null +++ b/static/js/dynamic-comment-form.js @@ -0,0 +1,94 @@ +/* + * dynamic-comment-form.js -- Captcha challenge loader + * Copyright © 2022 - 2023 Jakob L. Kreuze + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + */ + +function makeRequest (method, url) { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open(method, url); + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.response); + } else { + reject({ + status: xhr.status, + statusText: xhr.statusText + }); + } + }; + xhr.onerror = function () { + reject({ + status: xhr.status, + statusText: xhr.statusText + }); + }; + xhr.send(); + }); +} + +window.addEventListener("load", () => { + let primaryDisplay = document.getElementById("comment-form-primary"); + primaryDisplay.removeAttribute("hidden"); + + let altDisplay = document.getElementById("comment-form-alt"); + altDisplay.remove(); + + let trigger = document.getElementById("captcha-challenge-trigger"); + let triggerBlock = document.getElementById("captcha-trigger-block"); + trigger.addEventListener("click", (event) => { + let captchaField = document.getElementById("comment-captcha"); + + let captchaId = document.getElementById("captcha-id"); + let captchaImage = document.getElementById("captcha-image"); + + trigger.innerHTML = "Please wait..."; + trigger.disabled = true; + + if (captchaId.value === "") { + makeRequest("GET", "/api/challenge/captcha") + .then(function (data) { + // Update form with parsed values. + let challengeData = JSON.parse(data); + captchaId.value = challengeData["challenge-id"]; + captchaImage.src = challengeData["image"]; + + // Unhide fieldset. + captchaField.removeAttribute("hidden"); + + // Hide initial trigger. + triggerBlock.remove(); + }) + .catch(console.err); + } + + event.preventDefault(); + }); +}); + + +window.addEventListener("load", () => { + let header = document.getElementById("comment-form-header"); + let replyTo = document.getElementById("reply-to"); + let buttons = document.querySelectorAll(".comment-reply-button"); + buttons.forEach((button) => { + button.addEventListener("click", (event) => { + header.innerHTML = "Comment form (reply mode)"; + replyTo.value = button.getAttribute("data-reply-to-id"); + }); + }); +}); diff --git a/static/js/oneko.js b/static/js/oneko.js new file mode 100644 index 0000000..b0ba164 --- /dev/null +++ b/static/js/oneko.js @@ -0,0 +1,182 @@ +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the “Software”), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +(function oneko() { + const nekoEl = document.createElement("div"); + let nekoPosX = 32; + let nekoPosY = 32; + let mousePosX = 0; + let mousePosY = 0; + let frameCount = 0; + let idleTime = 0; + let idleAnimation = null; + let idleAnimationFrame = 0; + const nekoSpeed = 10; + const spriteSets = { + idle: [[-3, -3]], + alert: [[-7, -3]], + scratch: [ + [-5, 0], + [-6, 0], + [-7, 0], + ], + tired: [[-3, -2]], + sleeping: [ + [-2, 0], + [-2, -1], + ], + N: [ + [-1, -2], + [-1, -3], + ], + NE: [ + [0, -2], + [0, -3], + ], + E: [ + [-3, 0], + [-3, -1], + ], + SE: [ + [-5, -1], + [-5, -2], + ], + S: [ + [-6, -3], + [-7, -2], + ], + SW: [ + [-5, -3], + [-6, -1], + ], + W: [ + [-4, -2], + [-4, -3], + ], + NW: [ + [-1, 0], + [-1, -1], + ], + }; + function create() { + nekoEl.id = "oneko"; + nekoEl.style.width = "32px"; + nekoEl.style.height = "32px"; + nekoEl.style.position = "fixed"; + nekoEl.style.backgroundImage = "url('/static/image/oneko.gif')"; + nekoEl.style.imageRendering = "pixelated"; + nekoEl.style.left = "16px"; + nekoEl.style.top = "16px"; + + document.body.appendChild(nekoEl); + + document.onmousemove = (event) => { + mousePosX = event.clientX; + mousePosY = event.clientY; + }; + + window.onekoInterval = setInterval(frame, 100); + } + + function setSprite(name, frame) { + const sprite = spriteSets[name][frame % spriteSets[name].length]; + nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${ + sprite[1] * 32 + }px`; + } + + function resetIdleAnimation() { + idleAnimation = null; + idleAnimationFrame = 0; + } + + function idle() { + idleTime += 1; + + // every ~ 20 seconds + if ( + idleTime > 10 && + Math.floor(Math.random() * 200) == 0 && + idleAnimation == null + ) { + idleAnimation = ["sleeping", "scratch"][ + Math.floor(Math.random() * 2) + ]; + } + + switch (idleAnimation) { + case "sleeping": + if (idleAnimationFrame < 8) { + setSprite("tired", 0); + break; + } + setSprite("sleeping", Math.floor(idleAnimationFrame / 4)); + if (idleAnimationFrame > 192) { + resetIdleAnimation(); + } + break; + case "scratch": + setSprite("scratch", idleAnimationFrame); + if (idleAnimationFrame > 9) { + resetIdleAnimation(); + } + break; + default: + setSprite("idle", 0); + return; + } + idleAnimationFrame += 1; + } + + function frame() { + frameCount += 1; + const diffX = nekoPosX - mousePosX; + const diffY = nekoPosY - mousePosY; + const distance = Math.sqrt(diffX ** 2 + diffY ** 2); + + if (distance < nekoSpeed || distance < 48) { + idle(); + return; + } + + idleAnimation = null; + idleAnimationFrame = 0; + + if (idleTime > 1) { + setSprite("alert", 0); + // count down after being alerted before moving + idleTime = Math.min(idleTime, 7); + idleTime -= 1; + return; + } + + direction = diffY / distance > 0.5 ? "N" : ""; + direction += diffY / distance < -0.5 ? "S" : ""; + direction += diffX / distance > 0.5 ? "W" : ""; + direction += diffX / distance < -0.5 ? "E" : ""; + setSprite(direction, frameCount); + + nekoPosX -= (diffX / distance) * nekoSpeed; + nekoPosY -= (diffY / distance) * nekoSpeed; + + nekoEl.style.left = `${nekoPosX - 16}px`; + nekoEl.style.top = `${nekoPosY - 16}px`; + } + + create(); +})(); diff --git a/static/js/proof-of-work.js b/static/js/proof-of-work.js new file mode 100644 index 0000000..b4b5c57 --- /dev/null +++ b/static/js/proof-of-work.js @@ -0,0 +1,109 @@ +/* + * proof-of-work.js -- Alternative captcha based on SHA-256 proof-of-work. + * Copyright © 2022 - 2023 Jakob L. Kreuze + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + */ + +function makeid(length) { + let result = ''; + let alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + for (let i = 0; i < length; i++) { + result += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); + } + return result; +} + +async function digestMessage(message) { + const msgUtf8 = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest('SHA-256', msgUtf8); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); + return hashHex; +} + +async function findPrefix(hardness, nonce) { + while (true) { + let prefix = makeid(32); + let digestHex = await digestMessage(prefix + nonce); + if (digestHex.startsWith("0".repeat(hardness))) { + return prefix; + } + } +} + +function makeRequest (method, url) { + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open(method, url); + xhr.onload = function () { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.response); + } else { + reject({ + status: xhr.status, + statusText: xhr.statusText + }); + } + }; + xhr.onerror = function () { + reject({ + status: xhr.status, + statusText: xhr.statusText + }); + }; + xhr.send(); + }); +} + +function raceEndpoint() { + return new Promise(function (resolve, reject) { + makeRequest("GET", "/api/challenge/proof-of-work") + .then(function (data) { + let challengeData = JSON.parse(data); + findPrefix(challengeData.hardness, challengeData.nonce) + .then((prefix) => { resolve([prefix, challengeData["challenge-id"]]) } ); + }) + .catch(reject); + }); +} + +window.addEventListener("load", () => { + let trigger = document.getElementById("pow-trigger"); + trigger.removeAttribute("hidden"); + trigger.addEventListener("click", (e) => { + trigger.innerHTML = "Please wait..."; + trigger.disabled = true; + raceEndpoint() + .then((result) => { + let resultField = document.getElementById("captcha-alt"); + let challengeIdField = document.getElementById("captcha-alt-id"); + resultField.value = result[0]; + challengeIdField.value = result[1]; + trigger.innerHTML = "Proof-of-Work completed successfully!"; + trigger.disabled = true; + + // Hide the primary captcha, too, to make it clear that it isn't necessary to complete. + let primaryChallenge = document.getElementById("captcha-challenge-primary"); + primaryChallenge.hidden = true; + }) + .catch((err) => { + console.log(err); + trigger.innerHTML = "Proof-of-Work failed!"; + trigger.disabled = true; + }); + e.preventDefault(); + }) +}); diff --git a/static/js/rsvp.js b/static/js/rsvp.js new file mode 100644 index 0000000..06cdb66 --- /dev/null +++ b/static/js/rsvp.js @@ -0,0 +1,206 @@ +/* + * rsvp.js -- Simple web-based RSVP tool. + * Copyright © 2022 - 2023 Jakob L. Kreuze + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + */ + +// Common nodes. +const guestView = document.getElementById("guest-view"); +const addEntry = document.getElementById("add-entry"); +const removeEntry = document.getElementById("remove-entry"); +const submitButton = document.getElementById("submit-form"); +const form = document.getElementById("rsvp-input"); + +// Parse out the HTML form into something we can send to the backend. +function submitForm() { + let data = new FormData(form); + if (data.get("name").trim().length == 0) { + alert("You have to provide something for \"Name\"."); + return; + } else if (data.get("rsvp") === null) { + alert("You have to choose an option for \"RSVP Status\"."); + return; + } + + let res = { guests: [] }; + if (urlParams.get("i")) { + res["id"] = urlParams.get("i"); + } else if (urlParams.get("r")) { + res["update"] = urlParams.get("r"); + } + for (let pair of data.entries()) { + if (pair[0].startsWith("guest-")) { + res.guests.push(pair[1]); + } else if (pair[0] === "rsvp") { + const radioButtons = document.querySelectorAll('input[name="rsvp"]'); + for (const radioButton of radioButtons) { + if (radioButton.checked) { + res.rsvp = radioButton.id; + break; + } + } + } else { + res[pair[0]] = pair[1]; + } + } + res.guests = res.guests.join(","); + + let xhr = new XMLHttpRequest(); + xhr.open("POST", `${location.protocol}//${location.host}/api/rsvp`); + xhr.send(JSON.stringify(res)); + xhr.onload = function(data) { + if (xhr.status != 200) { + alert("Failed to submit form."); + } else { + document.getElementById("rsvp").innerHTML = ""; + let resp = JSON.parse(xhr.response); + let elem = document.createElement("p"); + elem.innerHTML = "Thanks for registering! Please bookmark or save the following link:"; + document.getElementById("rsvp").appendChild(elem); + + elem = document.createElement("a"); + elem.href = [location.protocol, '//', location.host, location.pathname].join('') + `?r=${resp.receipt}`; + elem.textContent = elem.href; + document.getElementById("rsvp").appendChild(elem); + + elem = document.createElement("p"); + elem.innerHTML = "This will enable you to edit your RSVP later."; + document.getElementById("rsvp").appendChild(elem); + } + } + xhr.onerror = function(error) { + throw new Error(`Request failed: ${error}`); + } +} + +// Immediately send off an XHR to load info from the backend. +const urlParams = new URLSearchParams(window.location.search); +if (!urlParams.get("i") && !urlParams.get("r")) { + document.getElementById("loading").remove(); + let elem = document.createElement("p"); + elem.innerHTML = "No invitation code provided."; + document.getElementById("rsvp").appendChild(elem); +} else { + let xhr = new XMLHttpRequest(); + if (urlParams.get("i")) { + xhr.open("GET", `${location.protocol}//${location.host}/api/rsvp/event-info?i=${urlParams.get("i")}`); + } else { + xhr.open("GET", `${location.protocol}//${location.host}/api/rsvp/event-info?r=${urlParams.get("r")}`); + } + xhr.send(); + xhr.onload = function(data) { + document.getElementById("loading").remove(); + if (xhr.status != 200) { + let elem = document.createElement("p"); + elem.innerHTML = "Invalid invitation code."; + document.getElementById("rsvp").appendChild(elem); + } else { + document.getElementById("rsvp-input").hidden = false; + + let response = JSON.parse(xhr.response); + + let elem = document.createElement("h1"); + elem.innerHTML = response.title; + document.getElementById("event-info").appendChild(elem); + + if (response.image !== "#f") { + elem = document.createElement("img"); + elem.src = `data:image/png;base64,${response.image}`; + elem.style = "float: right; max-size: 200px; margin: 16px;"; + document.getElementById("event-info").appendChild(elem); + } + + elem = document.createElement("p"); + elem.innerHTML = `Where: ${response.location}`; + document.getElementById("event-info").appendChild(elem); + + elem = document.createElement("p"); + elem.innerHTML = `When: ${response.date}`; + document.getElementById("event-info").appendChild(elem); + + elem = document.createElement("p"); + elem.innerHTML = response.description; + document.getElementById("event-info").appendChild(elem); + + if (response.hasOwnProperty("rsvps")) { + elem = document.createElement("h2"); + elem.innerHTML = "Current RSVPs"; + document.getElementById("rsvp-global").appendChild(elem); + + elem = document.createElement("table"); + document.getElementById("rsvp-global").appendChild(elem); + + for (let rsvp of response.rsvps) { + let container = document.createElement("tr"); + let field = document.createElement("td"); + field.textContent = rsvp.name; + container.append(field); + field = document.createElement("td"); + field.textContent = rsvp.email; + container.append(field); + field = document.createElement("td"); + field.textContent = rsvp.guests.replaceAll(',', ', '); + container.append(field); + field = document.createElement("td"); + field.textContent = rsvp.attending; + container.append(field); + elem.appendChild(container); + } + } + + // Pre-filled info if this is an edit. + if (response.hasOwnProperty("name")) { + document.getElementById("name").value = response.name; + } + if (response.hasOwnProperty("email")) { + document.getElementById("email").value = response.email; + } + if (response.hasOwnProperty("attending")) { + document.getElementById(response.attending).checked = true; + } + if (response.hasOwnProperty("guests") && response.guests.trim() != "") { + let i = 0; + for (let guest of response.guests.split(",")) { + let elem = document.createElement("input"); + elem.type = "text"; + elem.name = `guest-${i}`; + elem.value = guest; + guestView.appendChild(elem); + i++; + } + } + } + } + xhr.onerror = function(error) { + throw new Error(`Request failed: ${error}`); + } +} + +// Attach event listeners to the UI. +submitButton.addEventListener("click", submitForm); +submitButton.addEventListener("submit", submitForm); +addEntry.addEventListener("click", () => { + let i = guestView.childNodes.length; + let elem = document.createElement("input"); + elem.type = "text"; + elem.name = `guest-${i}`; + guestView.appendChild(elem); +}); +removeEntry.addEventListener("click", () => { + if (guestView.lastChild) { + guestView.lastChild.remove(); + } +}); diff --git a/static/js/section-folds.js b/static/js/section-folds.js new file mode 100644 index 0000000..9440264 --- /dev/null +++ b/static/js/section-folds.js @@ -0,0 +1,66 @@ +/* + * section-folds.js -- Parse out sections to hide behind an "unfold" dialog + * Copyright © 2022 - 2023 Jakob L. Kreuze + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + */ + +function handleFolds() { + function displayFold(fold, foldText, frontMatterText) { + foldText.innerHTML = `⬇️ ${frontMatterText}`; + fold.classList.remove("fold-hidden"); + + } + + function hideFold(fold, foldText, frontMatterText) { + foldText.innerHTML = `➡️ Folded ${frontMatterText}`; + fold.classList.add("fold-hidden"); + } + + let folds = document.querySelectorAll(".section-fold"); + + for (let fold of folds) { + // The first thing we'll do is generate the "front matter" for the fold, so + // that the reader knows what lies beneath the fold. + let frontMatterText = `Section on "${fold.dataset.name}"`; + let foldFrontMatter = document.createElement("div"); + + // The text will be inside of an anchor, which we'll hook onto using an + // event listener. + let foldAnchor = document.createElement("a"); + let foldText = document.createElement("p"); + foldAnchor.appendChild(foldText); + fold.parentNode.insertBefore(foldAnchor, fold.nextSibling); + + // The fold will start off in the "hidden" state. + let toggle = false; + hideFold(fold, foldText, frontMatterText); + + // But clicking on the "front matter" will cause the state of the fold + // to toggle, first to "visible", and then back to "hidden". + foldAnchor.addEventListener('click', () => { + toggle = !toggle; + if (toggle) { + displayFold(fold, foldText, frontMatterText); + } else { + hideFold(fold, foldText, frontMatterText); + } + }); + } + +} + +// Defer parsing all of the "folds" until the document has completely loaded. +document.addEventListener("DOMContentLoaded", handleFolds); diff --git a/static/js/webmention.js b/static/js/webmention.js new file mode 100644 index 0000000..d280d4d --- /dev/null +++ b/static/js/webmention.js @@ -0,0 +1,229 @@ +/* + * webmention.js -- Fetch and display mentions from webmention.io. + * Copyright © 2019 - 2023 Jakob L. Kreuze + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + */ + +function buildApiUri() { + const ref = "jakob.space"; + const slug = /[^\/]*(?=\.html)/.exec(window.location.href); + const endpoints = [ + `http://${ref}/${slug}.html`, + `https://${ref}/${slug}.html`, + `http://${ref}/blog/${slug}.html`, + `https://${ref}/blog/${slug}.html`, + ]; + return ["https://webmention.io/api/mentions.jf2?per-page=200&page=0"].concat( + endpoints.map((endpoint) => { + return `target[]=${endpoint}`; + }).join("&") + ).join(""); +} + +function getData(url, callback) { + if (fetch) { + fetch(url).then(function(response) { + if (response.status >= 200 && response.status < 300) { + return Promise.resolve(response); + } else { + return Promise.reject(new Error(`Request failed: ${response.statusText}`)); + } + }).then(function(response) { + return response.json(); + }).then(callback); + } else { + let xhr = new XMLHttpRequest(); + xhr.onload = function(data) { + callback(JSON.parse(data)); + } + xhr.onerror = function(error) { + throw new Error(`Request failed: ${error}`); + } + } +} + +function makeComment(comment) { + const strip = (uri) => { + const sep = "://"; + return uri.substring(uri.indexOf(sep) + sep.length); + }; + + const element = (type, attributes) => { + let res = document.createElement(type); + for (const attribute in attributes) { + res.setAttribute(attribute, attributes[attribute]); + } + return res; + }; + + // Create the h-card section. + let avatar = element("img", { + "class": "u-photo", + "src": comment.url.startsWith("https://lobste.rs/") + ? "/static/image/lobsters.png" + : comment.author.photo || "/static/image/default-icon.png" + }); + + let authorName = element("a", { + "class": "p-name u-url", + "href": comment.author.url + }); + authorName.textContent = comment.author.name; + + let authorURI = element("a", { + "class": "author_url", + "href": comment.author.url + }); + authorURI.textContent = strip(comment.author.url); + + let hCardContainer = element("div", { + "class": "p-author h-card author" + }); + hCardContainer.appendChild(avatar); + hCardContainer.appendChild(authorName); + hCardContainer.appendChild(authorURI); + + // Create the content section. + let contentContainer = element("div", { + "class": "e-content p-name comment-content", + }); + + if (comment.hasOwnProperty("content")) { + contentContainer.textContent = comment.content.text; + } else { + let em = element("em", {}); + if (comment.hasOwnProperty("wm-property")) { + if ("repost-of" === comment["wm-property"]) { + em.textContent = "Reposted this!"; + } else if ("like-of" === comment["wm-property"]) { + em.textContent = "Favorited this!"; + } else { + em.textContent = "No text provided :("; + } + } else { + em.textContent = "No text provided :("; + } + contentContainer.appendChild(em); + } + + // Create the metaline section. + let pubTime = comment.published || comment["wm-received"]; + let time = element("time", { + "class": "dt-published", + "datetime": pubTime, + }); + time.textContent = (new Date(pubTime)).toString(); + + let linkBack = element("a", { + "class": "u-url", + "href": comment.url + }); + linkBack.appendChild(time); + + let metalineContainer = element("div", { + "class": "metaline" + }); + metalineContainer.appendChild(linkBack); + + let sourceIcon = document.createElement("img"); + sourceIcon.classList.add("comment-source-identifier") + sourceIcon.src = "/static/image/webmention-logo.png"; + + // Put it all together. + let wrapper = element("li", { + "class": "p-comment h-cite comment comment-source-webmention", + }); + wrapper.appendChild(sourceIcon); + wrapper.appendChild(hCardContainer); + wrapper.appendChild(contentContainer); + wrapper.appendChild(metalineContainer); + + return wrapper; +} + +function makeInteraction(ent) { + const strip = (uri) => { + const sep = "://"; + return uri.substring(uri.indexOf(sep) + sep.length); + }; + + const element = (type, attributes) => { + let res = document.createElement(type); + for (const attribute in attributes) { + res.setAttribute(attribute, attributes[attribute]); + } + return res; + }; + + // Create the h-card section. + let avatar = element("img", { + "class": "u-photo", + "src": ent.url.startsWith("https://lobste.rs/") + ? "/static/image/lobsters.png" + : ent.author.photo || "/static/image/default-icon.png" + }); + let avatarContainer = element("a", { + "href": ent.author.url + }); + avatarContainer.appendChild(avatar); + + // Create the content section. + let contentContainer = element("div", { + "class": "e-content p-name comment-content", + }); + let em = element("em", {}); + if ("repost-of" === ent["wm-property"]) { + em.textContent = "Reposted this!"; + } else if ("like-of" === ent["wm-property"]) { + em.textContent = "Favorited this!"; + } else { + return null; + } + contentContainer.appendChild(em); + + let sourceIcon = document.createElement("img"); + sourceIcon.classList.add("comment-source-identifier") + sourceIcon.src = "/static/image/webmention-logo.png"; + + // Put it all together. + let wrapper = element("li", { + "class": "p-comment h-cite interaction comment-source-webmention", + }); + wrapper.appendChild(avatarContainer); + wrapper.appendChild(contentContainer); + wrapper.appendChild(sourceIcon); + + return wrapper; +} + +function makeEntry(ent) { + if (ent.hasOwnProperty("wm-property")) { + if (ent.hasOwnProperty("content") && ent["wm-property"] !== "repost-of") { + return makeComment(ent); + } else { + return makeInteraction(ent); + } + } + return null; +} + +getData(buildApiUri(), (json) => { + json.children + .map(makeEntry) + .filter((x) => x !== null) + .forEach((elem) => { + document.getElementById("webmention-container").appendChild(elem); + })}); -- cgit v1.3