Welcome to a series I call with joy where I clone popular websites/webapps with my web framework, joy.
If you’re just tuning in here’s Part 5.
Let’s look at where we are on the roadmap:
So we’re coming to the end of this, thank goodness. Profiles and follow/unfollow. Let’s do it
I have this pretty down pat at this point, you have two routes:
(route :post "/follows" :follows/create)
(route :delete "/follows/:id" :follows/delete)
And you hook them up with htmx:
(route :post "/follows" :follows/create)
(route :delete "/follows/:id" :follows/delete)
(defn follow-button [followed &opt follow]
[:form
[:input {:type "hidden" :name "followed-id" :value (get followed :id)}]
[:a (merge {:href "#" :hx-swap "outerHTML"}
(if follow
{:hx-delete (url-for :follows/delete follow)}
{:hx-post (url-for :follows/create)}
(if follow
icons/person-dash-fill)
icons/person-plus))]])
Then you can re-use some of the generated joy code. It’s super important to not do what I did before and to scope the follow by the account, you don’t want to let anyone unfollow anyone on their behalf!
(defn follow [req]
(def {:params params :account account} req)
; # scope it
(db/fetch [:account account :follow (get params :id)])))
(def follow-params
(params :follow
(validates [:followed-id] :required true)
(permit [:followed-id])))
(defn follows/create [req]
(def {:account account} req)
(def params (follow-params req))
(def result (-> (follow-params req)
(put :account-id (account :id))
(db/insert)
(rescue)))
(def [errors follow] result)
(text/html
(follow-button {:id (params :followed-id)} follow)))
(defn follows/delete [req]
(def follow (follow req))
(db/delete follow)
(text/html
(follow-button {:id (follow :followed-id)})))
Look at all of that re-use, wow. You can’t teach that!
This is the last feature that makes a complete social network and it fits within the controller model well at (route :get "/@*" :accounts/show)
This route uses wildcard params to get the account name (mostly because @:name
doesn’t work currently).
Here’s how the route looks:
(defn accounts/show [req]
(def {:wildcard params*} req)
(when-let [account (db/find-by :account :where {:name (get params* 0)})]
(def following (db/val "select count(id) from follow where followed_id = ?" (account :id)))
(def followers (db/val "select count(id) from follow where follower_id = ?" (account :id)))
(def likes (db/val "select count(id) from like where account_id = ?" (account :id)))
(def num-posts (db/val "select count(id) from post where account_id = ?" (account :id)))
(def posts (db/fetch-all [:account account :post] :order "post.created_at desc"))
[:vstack {:spacing "s" :class "w-100"}
[:img {:src (account :photo-url) :class "br-100 ba b--background-alt w-xl"}]
[:vstack {:spacing "s"}
[:vstack {:spacing "xs"}
[:strong (account :display-name)]
[:div {:class "muted"} (string "@" (account :name))]]
[:hstack {:spacing "s"}
[:hstack {:spacing "xs"}
[:b following]
[:span {:class "muted"} "Following"]]
[:hstack {:spacing "xs"}
[:b followers]
[:span {:class "muted"} "Followers"]]
[:hstack {:spacing "xs"}
[:b likes]
[:span {:class "muted"} "Likes"]]]]
[:vstack
(foreach [p posts]
(post (merge req {:post p})))]]))
At the end I kind of just punted on the sql stuff. Eventually joy should have more sql helpers, like count
, max
, distinct
, group
.
I cut it short because twitter has quite a few more functions, but this is the gist of it, or the gist of any follower based social network. There’s no recommendation algorithm here, which is probably a good exercise with the rise of interest based social networks like tiktok. Who wants to curate their followers up front when you can discover them based on a few interests you pick? NO ONE that’s who.
A few takeaways from this exercise: