Browse code
Add demo blog and gallery POCs
Ed Langley authored on 28/01/2019 10:59:48
Showing 2 changed files
Showing 2 changed files
1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,172 @@ |
1 |
+(defpackage :fwoar.blog |
|
2 |
+ (:use :cl :araneus :alexandria :serapeum :fw.lu) |
|
3 |
+ (:export )) |
|
4 |
+(in-package :fwoar.blog) |
|
5 |
+ |
|
6 |
+(defmacro new (class &rest initializer-syms) |
|
7 |
+ (multiple-value-bind (required optional rest) (parse-ordinary-lambda-list initializer-syms) |
|
8 |
+ (when optional |
|
9 |
+ (error "new doesn't handle optional arguments")) |
|
10 |
+ (if rest |
|
11 |
+ `(make-instance ,class |
|
12 |
+ ,@(mapcan (serapeum:op (list (alexandria:make-keyword _1) _1)) |
|
13 |
+ required) |
|
14 |
+ ,(make-keyword rest) ,rest) |
|
15 |
+ `(make-instance ,class |
|
16 |
+ ,@(mapcan (serapeum:op (list (alexandria:make-keyword _1) _1)) |
|
17 |
+ initializer-syms))))) |
|
18 |
+ |
|
19 |
+(defun-ct %constructor-name (class) |
|
20 |
+ (format nil "~a-~a" '#:make class)) |
|
21 |
+ |
|
22 |
+(defmacro make-constructor (class &rest args) |
|
23 |
+ (destructuring-bind (class &optional (constructor-name (intern (%constructor-name class)))) |
|
24 |
+ (ensure-list class) |
|
25 |
+ `(defgeneric ,constructor-name (,@args) |
|
26 |
+ (:method (,@args) |
|
27 |
+ (new ',class ,@args))))) |
|
28 |
+ |
|
29 |
+(defclass blog () |
|
30 |
+ ((%posts :initarg :posts :accessor posts)) |
|
31 |
+ (:default-initargs :posts ())) |
|
32 |
+(make-constructor blog &rest posts) |
|
33 |
+(defmethod print-object ((o blog) s) |
|
34 |
+ (format s "#.(make-blog ~{~s~^ ~})" |
|
35 |
+ (posts o))) |
|
36 |
+ |
|
37 |
+ |
|
38 |
+(defclass post () |
|
39 |
+ ((%content :initarg :content :accessor content))) |
|
40 |
+ |
|
41 |
+(defclass micropost (post) |
|
42 |
+ ()) |
|
43 |
+(make-constructor micropost content) |
|
44 |
+(defmethod print-object ((o micropost) s) |
|
45 |
+ (format s "#.(make-micropost ~s)" (content o))) |
|
46 |
+ |
|
47 |
+(defclass macropost (post) |
|
48 |
+ ((%title :initarg :title :accessor title))) |
|
49 |
+(make-constructor macropost title content) |
|
50 |
+(defmethod print-object ((o macropost) s) |
|
51 |
+ (format s "#.(make-macropost ~s ~s)" |
|
52 |
+ (title o) |
|
53 |
+ (content o))) |
|
54 |
+ |
|
55 |
+(defmacro orc (&rest funs) |
|
56 |
+ `(lambda (v) |
|
57 |
+ (or ,@(loop for fun in funs |
|
58 |
+ collect `(,fun v))))) |
|
59 |
+ |
|
60 |
+(defun slugify (string) |
|
61 |
+ (substitute #\- #\space |
|
62 |
+ (trim-whitespace |
|
63 |
+ (remove-if-not (orc alphanumericp |
|
64 |
+ (lambda (c) (eql c #\space)) |
|
65 |
+ (lambda (c) (eql c #\-))) |
|
66 |
+ (string-downcase string))))) |
|
67 |
+ |
|
68 |
+(defgeneric titled-posts (blog) |
|
69 |
+ (:method ((blog blog)) |
|
70 |
+ (mappend #'titled-posts |
|
71 |
+ (posts blog))) |
|
72 |
+ (:method ((post micropost)) |
|
73 |
+ ()) |
|
74 |
+ (:method ((post macropost)) |
|
75 |
+ (list post))) |
|
76 |
+ |
|
77 |
+(defgeneric find-post (slug blog) |
|
78 |
+ (:method ((slug string) (blog blog)) |
|
79 |
+ (loop with needle-slug = (slugify slug) |
|
80 |
+ for post in (titled-posts blog) |
|
81 |
+ for haystack-slug = (slugify (title post)) |
|
82 |
+ when (equal needle-slug haystack-slug) |
|
83 |
+ return post))) |
|
84 |
+ |
|
85 |
+(defclass blog-route () |
|
86 |
+ ((%blog :initarg :blog :reader blog))) |
|
87 |
+ |
|
88 |
+(defclass index (blog-route) |
|
89 |
+ ()) |
|
90 |
+(make-constructor (index make-blog-index)) |
|
91 |
+ |
|
92 |
+(defclass post-route (blog-route) |
|
93 |
+ ((%post :initarg :post :reader post))) |
|
94 |
+(make-constructor (post make-blog-post)) |
|
95 |
+ |
|
96 |
+(defmethod controller ((route index) params &key) |
|
97 |
+ (posts (blog route))) |
|
98 |
+ |
|
99 |
+(defmethod controller ((route post-route) params &key) |
|
100 |
+ (post route)) |
|
101 |
+ |
|
102 |
+(defmethod view ((name post-route) (post macropost)) |
|
103 |
+ (spinneret:with-html-string |
|
104 |
+ (:section |
|
105 |
+ (:h* (title post)) |
|
106 |
+ (:div |
|
107 |
+ (content post))))) |
|
108 |
+ |
|
109 |
+(defmethod view ((name index) posts) |
|
110 |
+ (spinneret:with-html-string |
|
111 |
+ (:section |
|
112 |
+ (:h* "Blog Index") |
|
113 |
+ (:div |
|
114 |
+ (loop for post in posts |
|
115 |
+ do (call-current-view post)))))) |
|
116 |
+ |
|
117 |
+(defmethod view ((name index) (post micropost)) |
|
118 |
+ (spinneret:with-html |
|
119 |
+ (:section.post.micropost |
|
120 |
+ (content post)))) |
|
121 |
+ |
|
122 |
+ |
|
123 |
+(defmethod view ((name index) (post macropost)) |
|
124 |
+ (spinneret:with-html |
|
125 |
+ (:section.post.macropost |
|
126 |
+ (:h* (:a :href (format nil "/~a" (slugify (title post))) |
|
127 |
+ (title post)))))) |
|
128 |
+ |
|
129 |
+(defun setup-routes (app blog) |
|
130 |
+ (defroutes app |
|
131 |
+ (("/" :method :GET) (as-route (make-instance 'index :blog blog))) |
|
132 |
+ (("/:post" :method :GET) (lambda (params) |
|
133 |
+ (format t "~¶ms: ~s~%" params) |
|
134 |
+ (let* ((post-name (cdr (assoc :post params))) |
|
135 |
+ (route (make-instance 'post-route :post (find-post post-name blog)))) |
|
136 |
+ (run-route route params)))))) |
|
137 |
+ |
|
138 |
+(defvar *blog* |
|
139 |
+ (make-blog (make-micropost "first post") |
|
140 |
+ (make-macropost "This is the title" |
|
141 |
+ "This is the post content")) |
|
142 |
+ "The sample blog: passed lexically to the routes, so rebinding don't change nothin'") |
|
143 |
+ |
|
144 |
+(defun setup (&optional (blog *blog*)) |
|
145 |
+ (prog1-bind (app (make-instance 'ningle:<app>)) |
|
146 |
+ (setup-routes app blog))) |
|
147 |
+ |
|
148 |
+;;; entrypoint |
|
149 |
+(defvar *handler*) |
|
150 |
+ |
|
151 |
+(defun is-running () |
|
152 |
+ (and (boundp '*handler*) |
|
153 |
+ *handler*)) |
|
154 |
+ |
|
155 |
+(defun ensure-started (&rest r &key port) |
|
156 |
+ (declare (ignore port)) |
|
157 |
+ (setf *handler* |
|
158 |
+ (if (not (is-running)) |
|
159 |
+ (apply 'clack:clackup (setup) r) |
|
160 |
+ *handler*))) |
|
161 |
+ |
|
162 |
+(defun stop () |
|
163 |
+ (if (is-running) |
|
164 |
+ (progn |
|
165 |
+ (clack:stop *handler*) |
|
166 |
+ (makunbound '*handler*) |
|
167 |
+ nil) |
|
168 |
+ nil)) |
|
169 |
+ |
|
170 |
+#+fw.dev |
|
171 |
+(define-cluser-entrypoint (&optional (port 5000)) |
|
172 |
+ (ensure-started :port port)) |
... | ... |
@@ -1,3 +1,14 @@ |
1 |
+#.(progn |
|
2 |
+ (asdf:defsystem :fwoar.gallery |
|
3 |
+ :depends-on (:lquery |
|
4 |
+ :araneus |
|
5 |
+ :drakma |
|
6 |
+ :clack |
|
7 |
+ :yason |
|
8 |
+ :ningle |
|
9 |
+ :lass)) |
|
10 |
+ nil) |
|
11 |
+ |
|
1 | 12 |
(defpackage :fwoar.gallery |
2 | 13 |
(:use :cl :araneus :alexandria :serapeum) |
3 | 14 |
(:export )) |
... | ... |
@@ -74,6 +85,7 @@ |
74 | 85 |
(defun get-site (uri) |
75 | 86 |
(hostname-case uri |
76 | 87 |
("gfycat.com" :gfycat) |
88 |
+ ("imgur.com" :imgur) |
|
77 | 89 |
("i.redd.it" :ireddit) |
78 | 90 |
("v.redd.it" :vreddit))) |
79 | 91 |
|
... | ... |
@@ -88,6 +100,13 @@ |
88 | 100 |
(fw.lu:prog1-bind (uri (puri:copy-uri uri)) |
89 | 101 |
(setf (puri:uri-host uri) "gfycat.com")) |
90 | 102 |
nil)))) |
103 |
+ (:method ((site (eql :imgur)) uri) |
|
104 |
+ (make-image |
|
105 |
+ (format nil "~a.jpg" |
|
106 |
+ (puri:render-uri |
|
107 |
+ (fw.lu:prog1-bind (uri (puri:copy-uri uri)) |
|
108 |
+ (setf (puri:uri-host uri) "i.imgur.com")) |
|
109 |
+ nil)))) |
|
91 | 110 |
(:method ((site (eql :vreddit)) uri) |
92 | 111 |
(make-video (puri:render-uri uri nil)))) |
93 | 112 |
|
... | ... |
@@ -112,11 +131,19 @@ |
112 | 131 |
body) |
113 | 132 |
))) |
114 | 133 |
|
115 |
-(defmethod view :around ((name (eql 'root)) (model page)) |
|
116 |
- (spinneret:with-html-string |
|
117 |
- (:html |
|
118 |
- (:head |
|
119 |
- (css |
|
134 |
+(spinneret:deftag js (body attrs) |
|
135 |
+ `(:script :type "text/javascript" |
|
136 |
+ ,@attrs |
|
137 |
+ (:raw |
|
138 |
+ ,(format nil "~%") |
|
139 |
+ (ps:ps |
|
140 |
+ ,@body)))) |
|
141 |
+ |
|
142 |
+(defun gallery-css () |
|
143 |
+ (spinneret:with-html |
|
144 |
+ (css |
|
145 |
+ (:let ((bottom-gap 2em) |
|
146 |
+ (gallery-height (calc (- 100vh 2em)))) |
|
120 | 147 |
(* |
121 | 148 |
:box-sizing border-box) |
122 | 149 |
(html |
... | ... |
@@ -127,22 +154,21 @@ |
127 | 154 |
|
128 | 155 |
(.gallery |
129 | 156 |
:position relative |
130 |
- :display flex |
|
157 |
+ :column-count 4 |
|
131 | 158 |
:width 100vw |
132 |
- :height (calc (- 100vh 2em)) |
|
159 |
+ :height #(gallery-height) |
|
133 | 160 |
:overflow-y scroll |
134 | 161 |
:background "#888" |
135 | 162 |
|
136 | 163 |
:flex-wrap wrap) |
137 | 164 |
((.gallery > div) |
138 | 165 |
:text-align center |
139 |
- :width 25% |
|
140 | 166 |
:max-height 25vh |
141 | 167 |
:overflow hidden) |
142 | 168 |
((.gallery img) |
143 | 169 |
:width 98% |
144 | 170 |
:height 98% |
145 |
- :object-fit contain) |
|
171 |
+ :object-fit cover) |
|
146 | 172 |
((.gallery video) |
147 | 173 |
:width 98% |
148 | 174 |
:height 98% |
... | ... |
@@ -151,7 +177,7 @@ |
151 | 177 |
:position fixed |
152 | 178 |
:top 0 |
153 | 179 |
:left 0 |
154 |
- :max-height (calc (- 100vh 2em)) |
|
180 |
+ :max-height #(gallery-height) |
|
155 | 181 |
:height 100% |
156 | 182 |
:width 100% |
157 | 183 |
:background "#eee") |
... | ... |
@@ -162,11 +188,35 @@ |
162 | 188 |
:height 100%) |
163 | 189 |
((.gallery > div.expanded > img) |
164 | 190 |
:display inline-block |
191 |
+ :object-fit contain |
|
165 | 192 |
:vertical-align middle |
166 | 193 |
:height (calc (- 100vh 2em))) |
167 | 194 |
((.gallery > div.expanded > video) |
168 | 195 |
:display inline-block |
169 |
- :vertical-align middle))) |
|
196 |
+ :vertical-align middle))))) |
|
197 |
+ |
|
198 |
+(defun gallery-js () |
|
199 |
+ (js |
|
200 |
+ (ps:chain #() for-each |
|
201 |
+ (call (ps:chain document |
|
202 |
+ (query-selector-all ".gallery > div")) |
|
203 |
+ (lambda (it) |
|
204 |
+ (ps:chain it (add-event-listener |
|
205 |
+ "click" |
|
206 |
+ (lambda () |
|
207 |
+ (ps:chain #() for-each |
|
208 |
+ (call (ps:chain document (query-selector-all ".expanded")) |
|
209 |
+ (lambda (other) |
|
210 |
+ (unless (eql other it) |
|
211 |
+ (ps:chain other class-list (remove "expanded")))))) |
|
212 |
+ (ps:chain it class-list (toggle "expanded"))))))))) ) |
|
213 |
+ |
|
214 |
+ |
|
215 |
+(defmethod view :around ((name (eql 'root)) (model page)) |
|
216 |
+ (spinneret:with-html-string |
|
217 |
+ (:html |
|
218 |
+ (:head |
|
219 |
+ (gallery-css)) |
|
170 | 220 |
(:body |
171 | 221 |
(let ((*gallery* (make-gallery (subseq (images (gallery model)) |
172 | 222 |
(* 52 (1- (page model))) |
... | ... |
@@ -178,22 +228,7 @@ |
178 | 228 |
(:a :href (format nil "/?page=~d" (1+ (page model))) |
179 | 229 |
:style "width: 100%; text-align: center; display: inline-block;" |
180 | 230 |
"next")) |
181 |
- (:script |
|
182 |
- (:raw |
|
183 |
- (ps:ps |
|
184 |
- (ps:chain #() for-each |
|
185 |
- (call (ps:chain document |
|
186 |
- (query-selector-all ".gallery > div")) |
|
187 |
- (lambda (it) |
|
188 |
- (ps:chain it (add-event-listener |
|
189 |
- "click" |
|
190 |
- (lambda () |
|
191 |
- (ps:chain #() for-each |
|
192 |
- (call (ps:chain document (query-selector-all ".expanded")) |
|
193 |
- (lambda (other) |
|
194 |
- (unless (eql other it) |
|
195 |
- (ps:chain other class-list (remove "expanded")))))) |
|
196 |
- (ps:chain it class-list (toggle "expanded"))))))))))))))) |
|
231 |
+ (gallery-js))))) |
|
197 | 232 |
|
198 | 233 |
(define-view root ((model page)) |
199 | 234 |
(spinneret:with-html |
... | ... |
@@ -215,6 +250,12 @@ |
215 | 250 |
(defroutes app |
216 | 251 |
(("/") (as-route 'root :gallery gallery)))) |
217 | 252 |
|
253 |
+(defun get-reddit-items (r) |
|
254 |
+ (process-uri-list |
|
255 |
+ (mapcar (lambda (i) |
|
256 |
+ (fw.lu:pick '("data" "url") i)) |
|
257 |
+ (fw.lu:pick '("data" "children") r)))) |
|
258 |
+ |
|
218 | 259 |
(defun main (url) |
219 | 260 |
(let* ((app (make-instance 'ningle:<app>)) |
220 | 261 |
(gallery (page->gallery (plump:parse |
... | ... |
@@ -224,12 +265,6 @@ |
224 | 265 |
(initialize-app app gallery) |
225 | 266 |
(clack:clackup app))) |
226 | 267 |
|
227 |
-(defun get-reddit-items (r) |
|
228 |
- (process-uri-list |
|
229 |
- (mapcar (lambda (i) |
|
230 |
- (fw.lu:pick '("data" "url") i)) |
|
231 |
- (fw.lu:pick '("data" "children") r)))) |
|
232 |
- |
|
233 | 268 |
(defun reddit-main (subreddits) |
234 | 269 |
(let* ((app (make-instance 'ningle:<app>)) |
235 | 270 |
(gallery (make-gallery |
... | ... |
@@ -245,3 +280,8 @@ |
245 | 280 |
(setf *gallery* gallery) |
246 | 281 |
(initialize-app app gallery) |
247 | 282 |
(clack:clackup app))) |
283 |
+ |
|
284 |
+(defun cl-user::fwoar.gallery.main (version init) |
|
285 |
+ (ecase version |
|
286 |
+ (:reddit (reddit-main init)) |
|
287 |
+ (:dir (main init)))) |