Browse code
feature: actually render the feeds
Ed Langley authored on 06/11/2019 07:55:28
Showing 8 changed files
Showing 8 changed files
- default-layout.lisp
- main.lisp
- package.lisp
- rss-reader.asd
- rss-render.lisp
- ui.lisp
- utils.lisp
- view.lisp
... | ... |
@@ -2,32 +2,52 @@ |
2 | 2 |
(:use :cl ) |
3 | 3 |
(:export |
4 | 4 |
#:default-layout |
5 |
- #:border-color)) |
|
5 |
+ #:border-color |
|
6 |
+ #:background-color |
|
7 |
+ #:text-color |
|
8 |
+ #:link-color)) |
|
6 | 9 |
(in-package :fwoar.default-layout) |
7 | 10 |
|
8 |
-(defclass default-layout () |
|
9 |
- ((%border-color :reader border-color :initform "#eee"))) |
|
10 |
- |
|
11 | 11 |
(defgeneric border-color (layout) |
12 | 12 |
) |
13 |
+(defgeneric text-color (layout) |
|
14 |
+ ) |
|
15 |
+(defgeneric link-color (layout) |
|
16 |
+ (:method (layout) |
|
17 |
+ (text-color layout))) |
|
18 |
+(defgeneric background-color (layout) |
|
19 |
+ ) |
|
20 |
+ |
|
21 |
+(defclass default-layout () |
|
22 |
+ ((%text-color :reader text-color :initform "#eee") |
|
23 |
+ (%border-color :reader border-color :initform "#eee") |
|
24 |
+ (%background-color :reader background-color :initform "#eee"))) |
|
13 | 25 |
|
14 | 26 |
(defmethod araneus:styles append ((layout default-layout)) |
15 | 27 |
`((body |
28 |
+ :background ,(background-color layout) |
|
29 |
+ :color ,(text-color layout) |
|
16 | 30 |
:display flex |
17 | 31 |
:flex-direction column |
18 | 32 |
:min-height 100vh) |
33 |
+ (a |
|
34 |
+ :color ,(link-color layout)) |
|
19 | 35 |
(h1 |
20 | 36 |
:height 3em |
21 |
- :border-bottom 1px solid ,(border-color layout) |
|
37 |
+ :border-bottom 4px double ,(border-color layout) |
|
22 | 38 |
:text-align center |
23 | 39 |
:line-height 3em) |
24 | 40 |
(div.main |
25 | 41 |
:display flex |
26 | 42 |
:width 100% |
27 |
- :flex-grow 1) |
|
43 |
+ :flex-grow 1 |
|
44 |
+ :flex-shrink 1 |
|
45 |
+ ) |
|
28 | 46 |
(nav |
29 |
- :width 10% |
|
47 |
+ :width 20rem |
|
30 | 48 |
:display flex |
49 |
+ :flex-grow 0 |
|
50 |
+ :flex-shrink 0 |
|
31 | 51 |
:flex-direction column) |
32 | 52 |
((nav a) |
33 | 53 |
:font-weight bold |
... | ... |
@@ -35,9 +55,11 @@ |
35 | 55 |
:text-align right |
36 | 56 |
:text-decoration underline) |
37 | 57 |
((nav (:and a :hover)) |
38 |
- :background "#eee") |
|
58 |
+ :background ,(link-color layout) |
|
59 |
+ :color ,(background-color layout)) |
|
39 | 60 |
(main |
61 |
+ :border-left 4px double ,(border-color layout) |
|
40 | 62 |
:flex-grow 1 |
63 |
+ :flex-shrink 1 |
|
41 | 64 |
:padding 1.5rem |
42 |
- :border-left 1px solid ,(border-color layout) |
|
43 |
- :flex-shrink 1))) |
|
65 |
+ :max-width 50em))) |
... | ... |
@@ -7,15 +7,25 @@ |
7 | 7 |
(:documentation "the clack handler representing the currently running application")) |
8 | 8 |
|
9 | 9 |
(defclass+ rss-reader () |
10 |
- ((%app :initform (make-instance 'ningle:<app>) |
|
10 |
+ ((%feeds :initarg :feeds :accessor feeds) |
|
11 |
+ (%app :initform (make-instance 'ningle:<app>) |
|
11 | 12 |
:reader app) |
12 | 13 |
(%handler :accessor handler)) |
13 | 14 |
(:documentation |
14 | 15 |
"A simple wrapper that ties a ningle app to a clack handler")) |
15 | 16 |
|
17 |
+ |
|
16 | 18 |
(defmethod araneus:routes progn ((app rss-reader)) |
17 | 19 |
(araneus:defroutes (app app) |
18 | 20 |
(("/") (serapeum:partial 'araneus:run-route (make-instance 'homepage))) |
21 |
+ (("/f/:feed") (lambda (params) |
|
22 |
+ (optima:match params |
|
23 |
+ ((optima.extra:alist (:feed . feed)) |
|
24 |
+ (araneus:run-route (feed-page |
|
25 |
+ (feeds app) |
|
26 |
+ (serapeum:assocdr feed (feeds app) |
|
27 |
+ :test 'string-equal)) |
|
28 |
+ params))))) |
|
19 | 29 |
(("/other") (serapeum:partial 'araneus:run-route 'other)))) |
20 | 30 |
|
21 | 31 |
(defun start (app) |
... | ... |
@@ -23,5 +33,16 @@ |
23 | 33 |
(clack:clackup (app app))) |
24 | 34 |
app) |
25 | 35 |
|
36 |
+(defvar *app*) |
|
37 |
+ |
|
38 |
+(defun unboundp (symbol) |
|
39 |
+ (not (boundp symbol))) |
|
40 |
+ |
|
26 | 41 |
(defun main () |
27 |
- (start (araneus:routes (rss-reader)))) |
|
42 |
+ (setf *app* |
|
43 |
+ (if (not (boundp '*app*)) |
|
44 |
+ (start |
|
45 |
+ (araneus:routes |
|
46 |
+ (rss-reader `((:techcrunch . "https://techcrunch.com/feed/") |
|
47 |
+ (:just-thomism . "https://thomism.wordpress.com/feed/"))))) |
|
48 |
+ *app*))) |
... | ... |
@@ -22,6 +22,7 @@ |
22 | 22 |
:components ((:file "package") |
23 | 23 |
(:file "utils" :depends-on ("package")) |
24 | 24 |
(:file "default-layout") |
25 |
+ (:file "rss-render") |
|
25 | 26 |
(:file "ui" :depends-on ("package")) |
26 |
- (:file "view" :depends-on ("package" "ui" "default-layout")) |
|
27 |
+ (:file "view" :depends-on ("package" "ui" "default-layout" "rss-render")) |
|
27 | 28 |
(:file "main" :depends-on ("package" "utils" "view")))) |
28 | 29 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,35 @@ |
1 |
+(defpackage :fwoar.rss-render |
|
2 |
+ (:use :cl ) |
|
3 |
+ (:export |
|
4 |
+ #:summary)) |
|
5 |
+(in-package :fwoar.rss-render) |
|
6 |
+ |
|
7 |
+(defmethod alimenta.render:render-feed (feed (renderer (eql 'summary))) |
|
8 |
+ (plump:parse |
|
9 |
+ (spinneret:with-html-string |
|
10 |
+ (:div.feed |
|
11 |
+ (:section |
|
12 |
+ (:h2.title (plump:decode-entities |
|
13 |
+ (alimenta:title feed))) |
|
14 |
+ (:p.description (plump:decode-entities |
|
15 |
+ (alimenta:description feed)))) |
|
16 |
+ (:ul.items))))) |
|
17 |
+ |
|
18 |
+(defmethod alimenta.render:render-item (item feed (renderer (eql 'summary))) |
|
19 |
+ (plump:first-element |
|
20 |
+ (plump:parse |
|
21 |
+ (spinneret:with-html-string |
|
22 |
+ (:li |
|
23 |
+ (:a :href (alimenta:link item) |
|
24 |
+ (plump:decode-entities |
|
25 |
+ (alimenta:title item))) |
|
26 |
+ (:p.description |
|
27 |
+ (plump:decode-entities |
|
28 |
+ (alimenta:description item)))))))) |
|
29 |
+ |
|
30 |
+(defmethod alimenta.render:add-rendered-item (feed-r item-r (renderer (eql 'summary))) |
|
31 |
+ (lquery:$ |
|
32 |
+ (inline feed-r) |
|
33 |
+ ".items" |
|
34 |
+ (append item-r)) |
|
35 |
+ feed-r) |
... | ... |
@@ -5,16 +5,24 @@ |
5 | 5 |
(:h1 "Hello, World!") |
6 | 6 |
(:div.main |
7 | 7 |
(:nav |
8 |
+ :data-ic-target "main" |
|
9 |
+ :data-ic-replace-target "true" |
|
8 | 10 |
(:a :data-ic-get-from "/" |
9 |
- :data-ic-target "#content" |
|
10 | 11 |
"home") |
11 | 12 |
(:a :data-ic-get-from "/other" |
12 |
- :data-ic-target "#content" |
|
13 | 13 |
"other") |
14 | 14 |
(:a :data-ic-get-from "/third" |
15 |
- :data-ic-target "#content" |
|
16 | 15 |
"third") |
17 | 16 |
(:a :data-ic-get-from "/fourth" |
18 |
- :data-ic-target "#content" |
|
19 | 17 |
"fourth")) |
20 |
- (:main#content content)))) |
|
18 |
+ (funcall content)))) |
|
19 |
+ |
|
20 |
+ |
|
21 |
+(defun intercooler-debugger () |
|
22 |
+ (ps |
|
23 |
+ (defun cdnjs-library (name version &key (file name)) |
|
24 |
+ (chain |
|
25 |
+ $ (get-script |
|
26 |
+ "https://cdnjs.cloudflare.com/ajax/libs/intercooler-js/1.2.2/intercooler-debugger.js"))) |
|
27 |
+ (cdnjs-library |
|
28 |
+ "intercooler-js" "1.2.2" #+(or)"intercooler-debugger.js"))) |
... | ... |
@@ -12,7 +12,12 @@ |
12 | 12 |
:initarg))) |
13 | 13 |
(make-symbol (symbol-name initarg))))) |
14 | 14 |
direct-slots)))) |
15 |
- `(progn (defclass ,name ,super |
|
15 |
+ `(progn (defclass ,name |
|
16 |
+ ,(mapcar (lambda (it) |
|
17 |
+ (typecase it |
|
18 |
+ (cons (car it)) |
|
19 |
+ (t it))) |
|
20 |
+ super) |
|
16 | 21 |
,direct-slots |
17 | 22 |
,@options) |
18 | 23 |
(defun ,name (,@initargs) |
... | ... |
@@ -1,23 +1,31 @@ |
1 | 1 |
(in-package :fwoar.rss-reader) |
2 | 2 |
|
3 | 3 |
(defclass rss-reader-route () |
4 |
+ ((%feed-list :initarg :feed-list :reader feed-list))) |
|
5 |
+ |
|
6 |
+(defclass+ intercooler-request () |
|
7 |
+ ((%request :initarg :request :reader request))) |
|
8 |
+(defclass+ partial-request ((intercooler-request (request))) |
|
9 |
+ ()) |
|
10 |
+(defclass+ full-request ((intercooler-request (request))) |
|
4 | 11 |
()) |
5 | 12 |
|
6 | 13 |
(defmethod araneus:view :around ((route rss-reader-route) model) |
7 | 14 |
"basic html and javascript for our app, as well as an invocation of |
8 | 15 |
the hook that pulls in CSS" |
9 | 16 |
|
10 |
- (ecase model |
|
11 |
- (:full |
|
17 |
+ (etypecase model |
|
18 |
+ (full-request |
|
12 | 19 |
(spinneret:with-html-string |
13 | 20 |
(:doctype) |
14 | 21 |
(:html |
15 | 22 |
(:head |
16 |
- (:script :src "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js" |
|
17 |
- :defer "defer") |
|
23 |
+ (:script :src "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js") |
|
18 | 24 |
(:script :src |
19 | 25 |
"https://cdnjs.cloudflare.com/ajax/libs/intercooler-js/1.2.2/intercooler.min.js" |
20 | 26 |
:defer "defer") |
27 |
+ (:script :defer "defer" |
|
28 |
+ (:raw (fwoar.rss-reader.ui:intercooler-debugger))) |
|
21 | 29 |
(:meta :name "intercoolerjs:use-data-prefix" |
22 | 30 |
:content "true"/) |
23 | 31 |
(:style |
... | ... |
@@ -25,7 +33,7 @@ the hook that pulls in CSS" |
25 | 33 |
(araneus:styles route)))) |
26 | 34 |
(:body |
27 | 35 |
(call-next-method))))) |
28 |
- (:partial |
|
36 |
+ (partial-request |
|
29 | 37 |
(spinneret:with-html-string |
30 | 38 |
(call-next-method))))) |
31 | 39 |
|
... | ... |
@@ -46,6 +54,47 @@ the hook that pulls in CSS" |
46 | 54 |
((:or ul ol) |
47 | 55 |
:padding-left 2rem))) |
48 | 56 |
|
57 |
+(defclass+ feed-page ((rss-reader-route (feed-list)) fwoar.default-layout:default-layout) |
|
58 |
+ ((%feed :initarg :feed :reader feed))) |
|
59 |
+ |
|
60 |
+(defmethod araneus:controller ((route feed-page) params &key) |
|
61 |
+ (funcall (if (equal (serapeum:assocdr "ic-request" params |
|
62 |
+ :test 'equal) |
|
63 |
+ "true") |
|
64 |
+ 'partial-request |
|
65 |
+ 'full-request) |
|
66 |
+ (alimenta.pull-feed:pull-feed (feed route)))) |
|
67 |
+ |
|
68 |
+(defmacro with-layout ((render-type layout) &body content) |
|
69 |
+ `(let ((content (lambda () |
|
70 |
+ (spinneret:with-html |
|
71 |
+ (:main |
|
72 |
+ ,@content))))) |
|
73 |
+ (ecase ,render-type |
|
74 |
+ (:full (spinneret:with-html |
|
75 |
+ (,layout content))) |
|
76 |
+ (:partial (funcall content))))) |
|
77 |
+ |
|
78 |
+(defmethod araneus:view ((_ feed-page) (model intercooler-request)) |
|
79 |
+ (spinneret:with-html |
|
80 |
+ (:main#content |
|
81 |
+ (alimenta:render (request model) |
|
82 |
+ 'fwoar.rss-render:summary)))) |
|
83 |
+(defmethod araneus:view ((route feed-page) (model full-request)) |
|
84 |
+ (spinneret:with-html |
|
85 |
+ (:h1 "Hello, World!") |
|
86 |
+ (:div.main |
|
87 |
+ (:nav |
|
88 |
+ :data-ic-push-url "true" |
|
89 |
+ :data-ic-target "main" |
|
90 |
+ :data-ic-replace-target "true" |
|
91 |
+ (loop for (feed-id) in (feed-list route) |
|
92 |
+ collect (:a :data-ic-get-from (format nil "/f/~a" (string-downcase feed-id)) |
|
93 |
+ feed-id))) |
|
94 |
+ (:main#content |
|
95 |
+ (alimenta:render (request model) |
|
96 |
+ 'fwoar.rss-render:summary))))) |
|
97 |
+ |
|
49 | 98 |
(defclass homepage (rss-reader-route fwoar.default-layout:default-layout) |
50 | 99 |
()) |
51 | 100 |
|
... | ... |
@@ -61,15 +110,14 @@ the hook that pulls in CSS" |
61 | 110 |
:full)) |
62 | 111 |
|
63 | 112 |
(defmethod araneus:view ((route homepage) model) |
64 |
- (let ((content "home content")) |
|
65 |
- (ecase model |
|
66 |
- (:full (fwoar.rss-reader.ui:homepage content)) |
|
67 |
- (:partial (spinneret:with-html |
|
68 |
- (:raw (with-output-to-string (s) |
|
69 |
- (cl-markdown:markdown content |
|
70 |
- :stream s)))))))) |
|
113 |
+ (with-layout (model fwoar.rss-reader.ui:homepage) |
|
114 |
+ (:raw |
|
115 |
+ (plump:serialize (alimenta:render (alimenta.pull-feed:pull-feed |
|
116 |
+ "https://thomism.wordpress.com/feed/") |
|
117 |
+ 'fwoar.rss-render:summary) |
|
118 |
+ nil)))) |
|
71 | 119 |
|
72 | 120 |
(araneus:define-controller other (_) |
73 | 121 |
(values)) |
74 | 122 |
(araneus:define-view other (_) |
75 |
- "<h2>???</h2>") |
|
123 |
+ "<main><h2>???</h2></main>") |