This commit is contained in:
Rüdiger Diedrich 2020-06-22 17:21:05 +02:00
parent 2984b4911e
commit a7ad35249a
10 changed files with 670 additions and 5 deletions

View File

@ -4,7 +4,9 @@
:min-lein-version "2.0.0" :min-lein-version "2.0.0"
:dependencies [[org.clojure/clojure "1.10.0"] :dependencies [[org.clojure/clojure "1.10.0"]
[compojure "1.6.1"] [compojure "1.6.1"]
[ring/ring-defaults "0.3.2"]] [ring/ring-defaults "0.3.2"]
[clj-http "3.10.1"]
[hickory "0.7.1"]]
:plugins [[lein-ring "0.12.5"]] :plugins [[lein-ring "0.12.5"]]
:ring {:handler clojsa.handler/app} :ring {:handler clojsa.handler/app}
:profiles :profiles

1
resources/public/css/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,448 @@
/* MVP.css v1.6.2 - https://github.com/andybrewer/mvp */
:root {
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color: #118bee;
--color-accent: #118bee15;
--color-bg: #fff;
--color-bg-secondary: #e9e9e9;
--color-secondary: #920de9;
--color-secondary-accent: #920de90b;
--color-shadow: #f4f4f4;
--color-text: #000;
--color-text-secondary: #999;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--hover-brightness: 1.2;
--justify-important: center;
--justify-normal: left;
--line-height: 1.5;
--width-card: 285px;
--width-card-medium: 460px;
--width-card-wide: 800px;
--width-content: 1080px;
}
/*
@media (prefers-color-scheme: dark) {
:root {
--color: #0097fc;
--color-accent: #0097fc4f;
--color-bg: #333;
--color-bg-secondary: #555;
--color-secondary: #e20de9;
--color-secondary-accent: #e20de94f;
--color-shadow: #bbbbbb20;
--color-text: #f7f7f7;
--color-text-secondary: #aaa;
}
}
*/
/* Layout */
article aside {
background: var(--color-secondary-accent);
border-left: 4px solid var(--color-secondary);
padding: 0.01rem 0.8rem;
}
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
line-height: var(--line-height);
margin: 0;
overflow-x: hidden;
padding: 1rem 0;
}
footer,
header,
main {
margin: 0 auto;
max-width: var(--width-content);
padding: 2rem 1rem;
}
hr {
background-color: var(--color-bg-secondary);
border: none;
height: 1px;
margin: 4rem 0;
}
section {
display: flex;
flex-wrap: wrap;
justify-content: var(--justify-important);
}
section aside {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
margin: 1rem;
padding: 1.25rem;
width: var(--width-card);
}
section aside:hover {
box-shadow: var(--box-shadow) var(--color-bg-secondary);
}
section aside img {
max-width: 100%;
}
[hidden] {
display: none;
}
/* Headers */
article header,
div header,
main header {
padding-top: 0;
}
header {
text-align: var(--justify-important);
}
header a b,
header a em,
header a i,
header a strong {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
header nav img {
margin: 1rem 0;
}
section header {
padding-top: 0;
width: 100%;
}
/* Nav */
nav {
align-items: center;
display: flex;
font-weight: bold;
justify-content: space-between;
margin-bottom: 7rem;
}
nav ul {
list-style: none;
padding: 0;
}
nav ul li {
display: inline-block;
margin: 0 0.5rem;
position: relative;
text-align: left;
}
/* Nav Dropdown */
nav ul li:hover ul {
display: block;
}
nav ul li ul {
background: var(--color-bg);
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
display: none;
height: auto;
left: -2px;
padding: .5rem 1rem;
position: absolute;
top: 1.7rem;
white-space: nowrap;
width: auto;
}
nav ul li ul li,
nav ul li ul li a {
display: block;
}
/* Typography */
code,
samp {
background-color: var(--color-accent);
border-radius: var(--border-radius);
color: var(--color-text);
display: inline-block;
margin: 0 0.1rem;
padding: 0 0.5rem;
}
details {
margin: 1.3rem 0;
}
details summary {
font-weight: bold;
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: var(--line-height);
}
mark {
padding: 0.1rem;
}
ol li,
ul li {
padding: 0.2rem 0;
}
p {
margin: 0.75rem 0;
padding: 0;
}
pre {
margin: 1rem 0;
max-width: var(--width-card-wide);
padding: 1rem 0;
}
pre code,
pre samp {
display: block;
max-width: var(--width-card-wide);
padding: 0.5rem 2rem;
white-space: pre-wrap;
}
small {
color: var(--color-text-secondary);
}
sup {
background-color: var(--color-secondary);
border-radius: var(--border-radius);
color: var(--color-bg);
font-size: xx-small;
font-weight: bold;
margin: 0.2rem;
padding: 0.2rem 0.3rem;
position: relative;
top: -2px;
}
/* Links */
a {
color: var(--color-secondary);
display: inline-block;
font-weight: bold;
text-decoration: none;
}
a:hover {
filter: brightness(var(--hover-brightness));
text-decoration: underline;
}
a b,
a em,
a i,
a strong,
button {
border-radius: var(--border-radius);
display: inline-block;
font-size: medium;
font-weight: bold;
line-height: var(--line-height);
margin: 0.5rem 0;
padding: 1rem 2rem;
}
button {
font-family: var(--font-family);
}
button:hover {
cursor: pointer;
filter: brightness(var(--hover-brightness));
}
a b,
a strong,
button {
background-color: var(--color);
border: 2px solid var(--color);
color: var(--color-bg);
}
a em,
a i {
border: 2px solid var(--color);
border-radius: var(--border-radius);
color: var(--color);
display: inline-block;
padding: 1rem 2rem;
}
/* Images */
figure {
margin: 0;
padding: 0;
}
figure img {
max-width: 100%;
}
figure figcaption {
color: var(--color-text-secondary);
}
/* Forms */
button:disabled,
input:disabled {
background: var(--color-bg-secondary);
border-color: var(--color-bg-secondary);
color: var(--color-text-secondary);
cursor: not-allowed;
}
button[disabled]:hover {
filter: none;
}
form {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow) var(--color-shadow);
display: block;
max-width: var(--width-card-wide);
min-width: var(--width-card);
padding: 1.5rem;
text-align: var(--justify-normal);
}
form header {
margin: 1.5rem 0;
padding: 1.5rem 0;
}
input,
label,
select,
textarea {
display: block;
font-size: inherit;
max-width: var(--width-card-wide);
}
input[type="checkbox"],
input[type="radio"] {
display: inline-block;
}
input[type="checkbox"]+label,
input[type="radio"]+label {
display: inline-block;
font-weight: normal;
position: relative;
top: 1px;
}
input,
select,
textarea {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
margin-bottom: 1rem;
padding: 0.4rem 0.8rem;
}
input[readonly],
textarea[readonly] {
background-color: var(--color-bg-secondary);
}
label {
font-weight: bold;
margin-bottom: 0.2rem;
}
/* Tables */
table {
border: 1px solid var(--color-bg-secondary);
border-radius: var(--border-radius);
border-spacing: 0;
display: inline-block;
max-width: 100%;
overflow-x: auto;
padding: 0;
white-space: nowrap;
}
table td,
table th,
table tr {
padding: 0.4rem 0.8rem;
text-align: var(--justify-important);
}
table thead {
background-color: var(--color);
border-collapse: collapse;
border-radius: var(--border-radius);
color: var(--color-bg);
margin: 0;
padding: 0;
}
table thead th:first-child {
border-top-left-radius: var(--border-radius);
}
table thead th:last-child {
border-top-right-radius: var(--border-radius);
}
table thead th:first-child,
table tr td:first-child {
text-align: var(--justify-normal);
}
table tr:nth-child(even) {
background-color: var(--color-accent);
}
/* Quotes */
blockquote {
display: block;
font-size: x-large;
line-height: var(--line-height);
margin: 1rem auto;
max-width: var(--width-card-medium);
padding: 1.5rem 1rem;
text-align: var(--justify-important);
}
blockquote footer {
color: var(--color-text-secondary);
display: block;
font-size: small;
line-height: var(--line-height);
padding: 1.5rem 0;
}

View File

@ -0,0 +1,20 @@
html {
font-size: 15pt;
}
body {
background-color: #ddd;
}
.box {
color: #222;
}
.container {
margin: 0 auto;
}
nav.pagination {
background-color: #fff;
margin-top: 4rem;
}

View File

@ -0,0 +1,22 @@
<svg width="16" height="16" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#494949">
<rect y="10" width="15" height="120" rx="6">
<animate attributeName="height" begin="0.5s" dur="1s" values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" repeatCount="indefinite"/>
<animate attributeName="y" begin="0.5s" dur="1s" values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" repeatCount="indefinite"/>
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate attributeName="height" begin="0.25s" dur="1s" values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" repeatCount="indefinite"/>
<animate attributeName="y" begin="0.25s" dur="1s" values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" repeatCount="indefinite"/>
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate attributeName="height" begin="0s" dur="1s" values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" repeatCount="indefinite"/>
<animate attributeName="y" begin="0s" dur="1s" values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" repeatCount="indefinite"/>
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate attributeName="height" begin="0.25s" dur="1s" values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" repeatCount="indefinite"/>
<animate attributeName="y" begin="0.25s" dur="1s" values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" repeatCount="indefinite"/>
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate attributeName="height" begin="0.5s" dur="1s" values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear" repeatCount="indefinite"/>
<animate attributeName="y" begin="0.5s" dur="1s" values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear" repeatCount="indefinite"/>
</rect>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

1
resources/public/js/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
document.body.addEventListener('configRequest.htmx', (e) => {
if (e.detail.verb != 'get') {
e.detail.parameters['__anti-forgery-token'] = __csrfToken;
}
});

View File

@ -1,11 +1,19 @@
(ns clojsa.handler (ns clojsa.handler
(:require [compojure.core :refer :all] (:require [clojsa.views :refer [index-page thread-page]]
[compojure.core :refer :all]
[compojure.route :as route] [compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]])) [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[ring.middleware.session :refer [wrap-session]]
[ring.middleware.session.cookie :refer (cookie-store)]))
(defroutes app-routes (defroutes app-routes
(GET "/" [] "Hello World") (GET "/" request (index-page request))
(GET "/thread/:id" [id page]
(thread-page (Integer/parseInt id) (Integer/parseInt page)))
(route/not-found "Not Found")) (route/not-found "Not Found"))
(def app (def app
(wrap-defaults app-routes site-defaults)) (-> app-routes
(wrap-defaults site-defaults)
(wrap-session {:cookie-attrs {:max-age 3600}
:store (cookie-store {:key "12345678abcdefgh"})})))

81
src/clojsa/saclient.clj Normal file
View File

@ -0,0 +1,81 @@
(ns clojsa.saclient
(:require [clojure.string :as string]
[clj-http.client :as client]
[hickory.core :refer :all]
[hickory.select :as s]
[hickory.convert :refer [hickory-to-hiccup]]))
(def url "https://forums.somethingawful.com/")
(defn thread-url
([id]
(thread-url id 1))
([id page]
(let [base-url (str url "showthread.php")
query {:threadid id
:pagenumber page}]
{:href base-url :params query})))
(defn thread-response [url]
(let [resp (client/get (:href url) {:query-params (:params url)})]
(:body resp)))
(def witcher-thread (thread-response (thread-url 3720352 3)))
(defn hickory-doc [doc]
(-> doc parse as-hickory))
(defn parse-title [htree]
(-> (s/select (s/child (s/tag :title)) htree)
first :content first))
(defn parse-pagecount [htree]
(-> (s/select (s/descendant
(s/class :pages) (s/tag :option)) htree)
last :content first Integer/parseInt))
(defn parse-thread [htree]
(-> (s/select (s/descendant
(s/id :thread))
htree)
first))
(defn select-td [class-key htree]
(s/select (s/descendant
(s/and (s/tag :td) (s/class class-key))) htree))
(defn parse-ui [ui]
(let [ui (first (s/select (s/descendant (s/tag :dl)) ui))]
(hickory-to-hiccup ui)))
(defn parse-pd [pd]
(string/trim (last (hickory-to-hiccup pd))))
(defn hickory-div [class content]
{:type :element,
:attrs {:class class},
:tag :div,
:content content})
(defn parse-pb [pb]
(let [pb (-> pb :content)]
(hickory-to-hiccup (hickory-div "postbody" pb))))
(defn thread-map [id doc]
(let [htree (hickory-doc doc)
title (parse-title htree)
page-count (parse-pagecount htree)
thread-tree (parse-thread htree)
userinfo (select-td :userinfo thread-tree)
postdate (select-td :postdate thread-tree)
postbody (select-td :postbody thread-tree)]
{:title title
:id id
:page-count page-count
:content
(for [[ui pd pb] (partition 3 (interleave userinfo postdate postbody))
:when (not= "Adbot" (-> (s/select (s/child (s/class :author)) ui)
first :content first))]
{:ui (parse-ui ui) :pd (parse-pd pd) :pb (parse-pb pb)})}))
(defn get-thread [id page]
(thread-map id (thread-response (thread-url id page))))

77
src/clojsa/views.clj Normal file
View File

@ -0,0 +1,77 @@
(ns clojsa.views
(:use [hiccup core page])
(:require [clojsa.saclient :refer [get-thread]]
[clojure.string :as string]
[clojure.java.io :as io]
[cheshire.core :as json]))
(defn header-fragment []
(html
[:nav]))
(defn main-template [opts & insert-body-here]
(html5
{:lang "de"}
[:head
[:meta {:charset "utf-8"}]
[:meta {:http-equiv "x-ua-compatible" :content "ie=edge"}]
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
[:title (get opts :title "clojsa")]
(include-css "/css/bulma.min.css")
(include-css "/css/style.css")
(include-js "/js/htmx.min.js")]
[:body
{:hx-boost "false"}
[:header
(header-fragment)]
[:main
insert-body-here]
(include-js "/js/main.js")]))
(defn index-page [req]
(let [thread (get-thread 3720352 3)]
(main-template {:title (:title thread)}
[:div.container
(for [post (:content thread)]
[:div.post (:pb post)])])))
(defn paginate [id cur last]
[:nav.container.pagination {:hx-boot "false"}
[:a.pagination-previous
{:href (format "/thread/%d?page=%d" id (dec cur))} "<"]
[:a.pagination-next
{:href (format "/thread/%s?page=%d" id (inc cur))} ">"]
[:ul.pagination-list
[:li
[:a.pagination-link
{:href (format "/thread/%d?page=%d" id 1)} (str 1)]]
[:li
[:span.pagination-ellipsis "&hellip;"]]
(for [i (range (- cur 2) (+ cur 3))]
[:li
[:a.pagination-link
{:href (format "/thread/%d?page=%d" id i)
:class (when (= i cur) "is-current")} (str i)]])
[:li
[:span.pagination-ellipsis "&hellip;"]]
[:li
[:a.pagination-link
{:href (format "/thread/%d?page=%d" id last)} (str last)]]]])
(defn thread-page [id page]
(let [thread (get-thread id page)
{:keys [id title page-count content]} thread]
(main-template
{:title title}
[:div.container
[:h1.is-size-3.mb-4 title]]
[:section.thread
(for [post content]
[:article.container.box.columns
[:aside.userinfo.column
(:ui post)]
[:main.postbody.content.column.is-four-fifths
(:pb post)]])]
[:section
(paginate id page page-count)])))