diff --git a/repl-sessions/db.clj b/repl-sessions/db.clj new file mode 100644 index 0000000..330ee35 --- /dev/null +++ b/repl-sessions/db.clj @@ -0,0 +1,16 @@ +(ns repl-sessions.db + (:require + [co.gaiwan.compass.db :as db] + [datomic.api :as d])) + +(db/q + '[:find + [(pull ?e [* {:session/type [*] + :session/location [*]}]) ...] + :where + [?e :session/title]] + (db/db)) +(deref + (db/transact [[:db/retractEntity 17592186045468]])) +(user/conn) + diff --git a/resources/public/css/styles.css b/resources/public/css/styles.css index 07334b5..5bf7e69 100644 --- a/resources/public/css/styles.css +++ b/resources/public/css/styles.css @@ -149,6 +149,13 @@ body { background-position: 50% 50%; } +.sessions-session_actions { + display: flex; + justify-content: flex-end; + width: 100%; + margin-top: var(--size-2); +} + .sessions-session_card { display: flex; gap: var(--size-1); @@ -219,25 +226,6 @@ body { display: block; } -.sessions-session_actions { - display: flex; - justify-content: flex-end; - width: 100%; - margin-top: var(--size-2); -} - -.home-filters { - display: flex; - flex-wrap: wrap; - margin-top: var(--size-3); - margin-bottom: var(--size-3); -} - -.home-filters button { - font-weight: 400; - flex-grow: 1; -} - .sessions-session_list { display: grid; gap: var(--size-3); @@ -266,4 +254,23 @@ body { grid-template-columns: repeat(4, 1fr); } +} + +.sessions-session_form form { + display: grid; + grid-template-columns: 10rem 1fr; + gap: var(--size-2); +} + +.home-filters { + display: flex; + flex-wrap: wrap; + gap: var(--size-1); + margin-top: var(--size-3); + margin-bottom: var(--size-3); +} + +.home-filters button, .home-filters .btn { + font-weight: 400; + flex-grow: 1; } \ No newline at end of file diff --git a/resources/public/js/cx.js b/resources/public/js/cx.js index 92f298c..a658ec9 100644 --- a/resources/public/js/cx.js +++ b/resources/public/js/cx.js @@ -1,11 +1,13 @@ // Additional utilties in the vein of HTMX, driven by HTML element attributes. -addEventListener("DOMContentLoaded", (_) => { - // cx-toggle : CSS class to toggle on click, either on clicked element, or on cx-target +// cx-toggle : CSS class to toggle on click, either on clicked element, or on cx-target +function handle_cx_toggle(e) { [...(document.querySelectorAll('[cx-toggle]'))].forEach((el) => { let klass = el.getAttribute('cx-toggle') let selector = el.getAttribute('cx-target') let target = selector ? el.closest(selector) : el; el.addEventListener("click", (_) => target.classList.toggle(klass)) }) -}); +} +addEventListener("DOMContentLoaded", handle_cx_toggle); +addEventListener("htmx:afterSwap", handle_cx_toggle); diff --git a/src/co/gaiwan/compass/db/schema.clj b/src/co/gaiwan/compass/db/schema.clj index 8f6481b..76b0b13 100644 --- a/src/co/gaiwan/compass/db/schema.clj +++ b/src/co/gaiwan/compass/db/schema.clj @@ -14,8 +14,8 @@ [:discord/expires-at :instant "Expiration timestamp for the OAuth2 token"] [:discord/refresh-token :string "Discord OAuth2 refresh-token"] - [:session/title :string "Title of the talk/workshop/activity" :identity] - [:session/subtitle :string "Subtitle of the session, for talks/workshops = speaker names" :identity] + [:session/title :string "Title of the talk/workshop/activity"] + [:session/subtitle :string "Subtitle of the session, for talks/workshops = speaker names"] [:session/description :string "Full description or abstract"] [:session/type :ref "Type of the session"] [:session/organized :ref "User who organizes this session"] @@ -31,7 +31,10 @@ [:session.type/name :string "Type of session, e.g. talk, activity"] [:session.type/color :string "CSS color or var reference used for rendering"] - [:location/name :string "Name of the location" :identity]]) + [:location/name :string "Name of the location" :identity] + [:oauth/state-id :uuid "State parameter passed along with the oauth flow" :identity] + [:oauth/redirect-url :string "Location to redirect to after login"] + ]) (defn inflate-schema [s] (for [[ident type doc & flags] s] @@ -50,3 +53,6 @@ (defn schema-tx [] (inflate-schema schema)) + +(comment + (user/reset)) diff --git a/src/co/gaiwan/compass/html/home.clj b/src/co/gaiwan/compass/html/home.clj index bc2adb4..7d6f7da 100644 --- a/src/co/gaiwan/compass/html/home.clj +++ b/src/co/gaiwan/compass/html/home.clj @@ -2,25 +2,23 @@ "Front page views and components" {:ornament/prefix "home-"} (:require - [co.gaiwan.compass.html.graphics :as graphics] [co.gaiwan.compass.html.sessions :as sessions] - [co.gaiwan.compass.css.tokens :as t :refer :all] - [co.gaiwan.compass.http.oauth :as oauth] - [co.gaiwan.compass.util :as util] - [clojure.datafy :as df] + [java-time.api :as time] [lambdaisland.ornament :as o])) (o/defstyled filters :section#filters - :flex :flex-wrap + :flex :flex-wrap :gap-1 :my-3 - [:button :font-normal :flex-grow] - ([] + [#{:button :.btn} :font-normal :flex-grow] + ([all-sessions] [:<> - [:button "Today"] - [:button "All"] + [:button "Today " (count (filter #(= (time/month-day) + (time/month-day (:session/time %))) + all-sessions))] + [:button "All " (count all-sessions)] [:button "My Activities"] [:button "Created By Me"] - [:button "Create An Activity"] + [:a.btn {:href "/sessions/new" :hx-boost "false"} "Create An Activity"] ])) ;; [:div @@ -61,36 +59,5 @@ (o/defstyled home :div ([{:keys [user sessions]}] [:<> - [filters] + [filters sessions] [sessions/session-list sessions]])) - -(comment - [sessions/session-card (sessions/rand-session)] - -;; Not in use now - (defn week-day-str [day] - (let [week-days ["Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday"]] - (nth week-days (dec day)))) - - (defn session-model - "session data from database to frontend - - input `x` is a vecotr of - `[session-graph type-keyword location-name]`" - [v] - (let [[session-graph type-keyword location-name] v - {:session/keys [time speaker name capacity organized - duration]} session-graph - {:keys [day-of-week month day-of-month - hour minute]} (df/datafy time) - day-of-week-str (week-day-str day-of-week) - date-str (format "%02d.%02d" day-of-month month) - time-str (format "%02d:%02d" hour minute)] - {:title name - :speaker speaker - :type type-keyword - :organized organized - :day day-of-week-str - :date date-str - :time time-str - :location location-name}))) diff --git a/src/co/gaiwan/compass/html/navigation.clj b/src/co/gaiwan/compass/html/navigation.clj index f30c8b0..b0a3e7a 100644 --- a/src/co/gaiwan/compass/html/navigation.clj +++ b/src/co/gaiwan/compass/html/navigation.clj @@ -5,15 +5,6 @@ [co.gaiwan.compass.http.oauth :as oauth] [lambdaisland.ornament :as o])) -(defn hx-a - "Use like :a, but uses htmx to only swap out the
element, so everything - but the navbar and menu." - [attrs & children] - (into [:a attrs #_(assoc attrs - :hx-target "main" :hx-select "main" - :hx-get (get attrs :href))] - children)) - (o/defstyled nav-bar :nav :flex :items-center :bg-surface-1 @@ -24,7 +15,7 @@ ([user] [:<> [graphics/compass-logo] - [:h1 [hx-a {:href "/"} "Compass"]] + [:h1 [:a {:href "/"} "Compass"]] [:button {:cx-toggle "menu-open" :cx-target "body"} [graphics/hamburger]]])) diff --git a/src/co/gaiwan/compass/html/sessions.clj b/src/co/gaiwan/compass/html/sessions.clj index 7c68196..7be001b 100644 --- a/src/co/gaiwan/compass/html/sessions.clj +++ b/src/co/gaiwan/compass/html/sessions.clj @@ -121,47 +121,50 @@ ;; Create / edit (o/defstyled session-form :div + [:form :grid {:grid-template-columns "10rem 1fr"} :gap-2] ([params] [:<> [:h2 "Create Activity"] [:form {:method "POST" :action "/sessions"} - [:div - [:label {:for "name"} "Activity Name"] - [:input {:id "name" :name "name" :type "text"}]] + [:label {:for "title"} "Title"] + [:input {:id "title" :name "title" :type "text"}] - [:div - [:label {:for "type"} "Type"] - [:select {:id "type" :name "type"} - [:option {:value "activity"} "activity"]]] + [:label {:for "subtitle"} "Subtitle"] + [:input {:id "subtitle" :name "subtitle" :type "text"}] - [:div - [:label {:for "location"} "Location"] - [:select {:id "location" :name "location"} - [:option {:value "depot-main-stage"} "Het Depot - main stage"] - [:option {:value "depot-bar"} "Het Depot - Bar"] - [:option {:value "hal5-zone-a"} "Hal 5 - zone A"] - [:option {:value "hal5-zone-b"} "Hal 5 - zone B"] - [:option {:value "hal5-hoc-cafe"} "Hal 5 - HoC Café"] - [:option {:value "hal5-foodcourt"} "Hal 5 - Foodcourt"] - [:option {:value "hal5-park"} "Hal 5 - park"] - [:option {:value "hal5-outside-seating"} "Hal 5 - outside seating"] - [:option {:value "hal5-long-table"} "Hal 5 - long table"]]] + [:label {:for "start-time"} "Start Time"] + [:input {:id "start-time" :name "start-time" :type "datetime-local"}] - [:div - [:label {:for "capacity"} "Capacity"] - [:input {:id "capacity" :name "capacity" :type "number"}]] + [:label {:for "end-time"} "End Time"] + [:input {:id "end-time" :name "end-time" :type "datetime-local"}] - [:div - [:label {:for "description"} "Description"] - [:textarea {:id "description" :name "description"}]] + [:label {:for "type"} "Type"] + [:select {:id "type" :name "type"} + [:option {:value "activity"} "activity"]] - [:div - [:label {:for "ticket"} "Requires Ticket?"] - [:input {:id "ticket" :name "ticket-required?" :type "checkbox"}]] + [:label {:for "location"} "Location"] + [:select {:id "location" :name "location"} + [:option {:value "depot-main-stage"} "Het Depot - main stage"] + [:option {:value "depot-bar"} "Het Depot - Bar"] + [:option {:value "hal5-zone-a"} "Hal 5 - zone A"] + [:option {:value "hal5-zone-b"} "Hal 5 - zone B"] + [:option {:value "hal5-hoc-cafe"} "Hal 5 - HoC Café"] + [:option {:value "hal5-foodcourt"} "Hal 5 - Foodcourt"] + [:option {:value "hal5-park"} "Hal 5 - park"] + [:option {:value "hal5-outside-seating"} "Hal 5 - outside seating"] + [:option {:value "hal5-long-table"} "Hal 5 - long table"]] - [:div - [:label {:for "published"} "Published/Visible?"] - [:input {:id "published" :name "published?" :type "checkbox"}]] + [:label {:for "capacity"} "Capacity"] + [:input {:id "capacity" :name "capacity" :type "number" :value 0}] + + [:label {:for "description"} "Description"] + [:textarea {:id "description" :name "description"}] + + [:label {:for "ticket"} "Requires Ticket?"] + [:input {:id "ticket" :name "ticket-required?" :type "checkbox"}] + + [:label {:for "published"} "Published/Visible?"] + [:input {:id "published" :name "published?" :type "checkbox"}] [:input {:type "submit" :value "Create"}]]])) diff --git a/src/co/gaiwan/compass/http/oauth.clj b/src/co/gaiwan/compass/http/oauth.clj index a1c20dd..6001eaf 100644 --- a/src/co/gaiwan/compass/http/oauth.clj +++ b/src/co/gaiwan/compass/http/oauth.clj @@ -12,16 +12,24 @@ (def discord-oauth-endpoint "https://discord.com/oauth2/authorize") (def discord-api-endpoint "https://discord.com/api/v10") +(def default-scopes ["email" "identify"]) + (defn flow-init-url ([] - (flow-init-url ["email" "identify"])) - ([scopes] - (-> (uri/uri discord-oauth-endpoint) - (uri/assoc-query* - {:client_id (config/value :discord/client-id) - :response_type "code" - :redirect_uri (str (config/value :compass/origin) "/oauth2/discord/callback") - :scope (str/join " " scopes)})))) + (flow-init-url nil)) + ([{:keys [scopes redirect-url] + :or {scopes default-scopes}}] + (let [state (random-uuid)] + (when redirect-url + @(db/transact [{:oauth/state-id state + :oauth/redirect-url redirect-url}])) + (-> (uri/uri discord-oauth-endpoint) + (uri/assoc-query* + {:client_id (config/value :discord/client-id) + :response_type "code" + :redirect_uri (str (config/value :compass/origin) "/oauth2/discord/callback") + :scope (str/join " " scopes) + :state state}))))) ;; Add as bot to server #_ @@ -47,7 +55,9 @@ (defn GET-callback [{:keys [query-params]}] (let [code (get query-params "code") - {:keys [status body]} (exchange-code code)] + state (get query-params "state") + {:keys [status body]} (exchange-code code) + redirect-url (:oauth/redirect-url (db/entity [:oauth/state-id (java.util.UUID/fromString state)]))] (if (not= 200 status) {:status 302 :headers {"Location" "/"} @@ -70,7 +80,7 @@ (def tx-data tx-data) @(db/transact tx-data ) {:status 302 - :headers {"Location" "/"} + :headers {"Location" (or redirect-url "/")} :flash [:p "You are signed in!"] :session {:identity user-uuid}})))) diff --git a/src/co/gaiwan/compass/routes/home.clj b/src/co/gaiwan/compass/routes/home.clj index 6b4e5e1..1832f6f 100644 --- a/src/co/gaiwan/compass/routes/home.clj +++ b/src/co/gaiwan/compass/routes/home.clj @@ -32,9 +32,6 @@ :sessions (all-sessions {:type "all" :location "all"})}]}) -(defn GET-filters [req] - {:html/body [h/filters]}) - (defn GET-conf-sessions [req] (let [qs-m (uri/query-string->map (:query-string req))] {:html/head [:title "sessions"] @@ -45,9 +42,5 @@ ["/" {:name :index :get {:handler GET-home}}] - ["/show-filters" - {:get {:handler GET-filters-showed}}] - ["/hide-filters" - {:get {:handler GET-filters-hidden}}] ["/conf-sessions" {:get {:handler GET-conf-sessions}}]]) diff --git a/src/co/gaiwan/compass/routes/sessions.clj b/src/co/gaiwan/compass/routes/sessions.clj index e062c88..4cfc79c 100644 --- a/src/co/gaiwan/compass/routes/sessions.clj +++ b/src/co/gaiwan/compass/routes/sessions.clj @@ -7,53 +7,69 @@ organized by participants. " (:require + [co.gaiwan.compass.db :as db] [co.gaiwan.compass.html.sessions :as h] - [ring.util.response :as response] - [co.gaiwan.compass.db :as db])) + [co.gaiwan.compass.http.oauth :as oauth] + [co.gaiwan.compass.util :as util] + [java-time.api :as time])) (defn new-session [req] (if-not (:identity req) - (assoc (response/redirect "/") - :flash "Please log in first") + (util/redirect (oauth/flow-init-url {:redirect-url "/sessions/new"})) {:html/head [:title "Create new session"] :html/body [h/session-form {}]})) +(defn GET-session [req] + {:html/body (pr-str (db/entity (parse-long (get-in req [:path-params :id]))))}) + (defn params->session-data "convert the Http Post Params to data ready for DB transaction" - [{:keys [name description + [{:keys [title subtitle start-time end-time description type location capacity ticket-required? published?]}] - (cond-> {:session/title name - :session/description description - :session/type (keyword "session.type" type) - :session/location (keyword "location.type" location) - :session/capacity (Integer/parseInt capacity)} - (= ticket-required? "on") - (assoc :session/ticket-required? true) - (= published? "on") - (assoc :session/published? true))) + (let [start (time/zoned-date-time start-time db/event-time-zone) + end (time/zoned-date-time end-time db/event-time-zone) + duration (time/duration start end)] + (cond-> {:db/id "session" + :session/title title + :session/subtitle subtitle + :session/time start + :session/duration (str duration) + :session/description description + :session/type (keyword "session.type" type) + :session/location (keyword "location.type" location) + :session/capacity (Integer/parseInt capacity)} + (= ticket-required? "on") + (assoc :session/ticket-required? true) + (= published? "on") + (assoc :session/published? true)))) (defn save-session "Save session to Datomic - + The typical params is: {:name \"dsafa\", :description \"dsafa\", - :type \"activity\", + :type \"activity\", :location \"depot-main-stage\", - :capacity \"34\", + :capacity \"34\", :ticket-required? \"on\" :published? \"on\"}" [{:keys [params]}] - (db/transact [(params->session-data params)]) - {:html/body [:p "OK " (pr-str params)]}) + (let [{:keys [tempids]} @(db/transact [(params->session-data params)])] + (def tempids tempids) + (util/redirect ["/sessions" (get tempids "session")] + {:flash "Successfully created!"}))) (defn routes [] ["/sessions" ["" {:name :activity/save :post {:handler save-session}}] - ["/new" - {:name :activity/new - :get {:handler new-session}}]]) + ["/:id" + {:get {:handler (fn [req] + (if (= "new" (get-in req [:path-params :id])) + (new-session req) + (GET-session req)))}}] + ]) diff --git a/src/co/gaiwan/compass/util.clj b/src/co/gaiwan/compass/util.clj index f1b9992..fb7feca 100644 --- a/src/co/gaiwan/compass/util.clj +++ b/src/co/gaiwan/compass/util.clj @@ -1,13 +1,15 @@ (ns co.gaiwan.compass.util - (:import [java.time Instant ZonedDateTime ZoneId] - [java.time.format DateTimeFormatter]) (:require [clojure.core.protocols :as p] [clojure.datafy :as d] - [clojure.pprint :as pprint])) + [clojure.pprint :as pprint] + [clojure.string :as str] + [ring.util.response :as response]) + (:import + (java.time Instant ZonedDateTime ZoneId))) (defn datafy-instant - "Output: + "Output: ``` {:hour LONG, :minute LONG, @@ -38,3 +40,19 @@ (defn pprint-str [o] (with-out-str (pprint/pprint o))) + +(defn redirect + "Returns a Ring response for an HTTP 302 redirect. Status may be + a key in redirect-status-codes or a numeric code. Defaults to 302" + ([url] + (redirect url :found)) + ([url {:keys [status flash push-url?]}] + (let [url (str (if (vector? url) + (str/join "/" url) + url))] + (cond-> {:status (response/redirect-status-codes status status) + :headers {"Location" url + "HX-Location" url} + :body ""} + flash + (assoc :flash flash)))))