diff --git a/src/quo2/components/calendar/calendar/component_spec.cljs b/src/quo2/components/calendar/calendar/component_spec.cljs new file mode 100644 index 00000000000..f30ea88f6a2 --- /dev/null +++ b/src/quo2/components/calendar/calendar/component_spec.cljs @@ -0,0 +1,64 @@ +(ns quo2.components.calendar.calendar.component-spec + (:require [quo2.components.calendar.calendar.view :as calendar] + [test-helpers.component :as h] + [cljs-time.core :as time])) + +(def start-date (time/date-time (time/year (time/now)) (time/month (time/now)) 5)) +(def end-date (time/date-time (time/plus start-date (time/days 2)))) + +(h/describe "calendar component" + (h/test "default render of calendar component" + (h/render + [calendar/view + {:start-date start-date + :end-date end-date}]) + (-> (h/expect (h/query-by-translation-text "Mo")) + (h/is-truthy))) + + (h/test "should call on-change with selected date on first click" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date nil + :end-date nil + :on-change on-change}]) + (h/fire-event :press (h/query-by-text (str (time/day start-date)))) + (h/was-called-with on-change {:start-date start-date :end-date nil}))) + + (h/test "should call on-change with start and end date on second click" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date start-date :end-date nil :on-change on-change}]) + (h/fire-event :press (h/query-by-text (str (time/day end-date)))) + (h/was-called-with on-change {:start-date start-date :end-date end-date}))) + + (h/test "should reset the dates on third click" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date start-date + :end-date end-date + :on-change on-change}]) + (h/fire-event :press (h/query-by-text (str (time/day start-date)))) + (h/was-called-with on-change {:start-date start-date :end-date nil}))) + + (h/test "should reset dates when start date is clicked again" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date start-date + :end-date nil + :on-change on-change}]) + (h/fire-event :press (h/query-by-text (str (time/day start-date)))) + (h/was-called-with on-change {:start-date nil :end-date nil}))) + + (h/test "should assign start and end date correctly when upper range selected first" + (let [on-change (h/mock-fn)] + (h/render + [calendar/view + {:start-date end-date + :end-date nil + :on-change on-change}]) + (h/fire-event :press (h/query-by-text (str (time/day start-date)))) + (h/was-called-with on-change {:start-date start-date :end-date end-date})))) diff --git a/src/quo2/components/calendar/calendar/days_grid/style.cljs b/src/quo2/components/calendar/calendar/days_grid/style.cljs new file mode 100644 index 00000000000..25d1d63635b --- /dev/null +++ b/src/quo2/components/calendar/calendar/days_grid/style.cljs @@ -0,0 +1,7 @@ +(ns quo2.components.calendar.calendar.days-grid.style) + +(def container-days + {:flex-grow 1 + :margin-top 4 + :margin-horizontal 8 + :overflow :hidden}) diff --git a/src/quo2/components/calendar/calendar/days_grid/utils.cljs b/src/quo2/components/calendar/calendar/days_grid/utils.cljs new file mode 100644 index 00000000000..594fd95b5f4 --- /dev/null +++ b/src/quo2/components/calendar/calendar/days_grid/utils.cljs @@ -0,0 +1,65 @@ +(ns quo2.components.calendar.calendar.days-grid.utils + (:require + [utils.number :as utils.number] + [cljs-time.core :as time])) + +(defn- day-of-week + [date] + (let [day (time/day-of-week date)] + (mod day 7))) + +(defn- add-days + [date days] + (time/plus date (time/days days))) + +(defn day-grid + [year month] + (let [year (utils.number/parse-int year) + month (utils.number/parse-int month) + first-day (time/date-time year month 1) + start-day (add-days first-day (- 0 (day-of-week first-day))) + end-day (add-days start-day 34)] + (loop [days [] + current-day start-day] + (if (time/after? current-day end-day) + days + (recur (conj days current-day) (add-days current-day 1)))))) + +(defn get-day-state + [day today year month start-date end-date] + (cond + (and start-date (time/equal? day start-date)) :selected + (and end-date (time/equal? day end-date)) :selected + (and (= (time/year day) (time/year today)) + (= (time/month day) (time/month today)) + (= (time/day day) (time/day today))) :today + (and (= (time/year day) year) (= (time/month day) month)) :default + :else :disabled)) + +(defn update-range + [day start-date end-date] + (let [new-state (cond + (and start-date end-date) {:start-date day :end-date nil} + (and start-date (time/equal? day start-date)) {:start-date nil :end-date nil} + (and end-date (time/equal? day end-date)) {:start-date nil :end-date nil} + (nil? start-date) {:start-date day :end-date nil} + (nil? end-date) {:start-date start-date :end-date day} + :else {:start-date start-date + :end-date end-date})] + (if (and (:start-date new-state) + (:end-date new-state) + (time/after? (:start-date new-state) (:end-date new-state))) + {:start-date (:end-date new-state) :end-date (:start-date new-state)} + new-state))) + +(defn in-range? + [day start-date end-date] + (and start-date end-date (time/after? day start-date) (time/before? day end-date))) + +(defn get-in-range-pos + [day start-date end-date] + (cond + (or (nil? start-date) (nil? end-date)) nil + (and start-date (time/equal? day start-date)) :start + (and end-date (time/equal? day end-date)) :end + (in-range? day start-date end-date) :middle)) diff --git a/src/quo2/components/calendar/calendar/days_grid/utils_test.cljs b/src/quo2/components/calendar/calendar/days_grid/utils_test.cljs new file mode 100644 index 00000000000..a4c0b293ee4 --- /dev/null +++ b/src/quo2/components/calendar/calendar/days_grid/utils_test.cljs @@ -0,0 +1,51 @@ +(ns quo2.components.calendar.calendar.days-grid.utils-test + (:require [cljs.test :refer-macros [deftest is testing]] + [quo2.components.calendar.calendar.days-grid.utils :as utils] + [cljs-time.core :as time])) + +(deftest day-grid-test + (let [day-grid-result (utils/day-grid "2023" "7")] + (testing "it returns correct days grid" + (is (= 35 (count day-grid-result))) + (is (time/equal? (time/date-time 2023 6 25) (first day-grid-result))) + (is (time/equal? (time/date-time 2023 7 29) (last day-grid-result)))))) + +(deftest get-day-state-test + (let [today (time/date-time 2023 7 27) + year 2023 + month 7 + start-date (time/date-time 2023 7 20) + end-date (time/date-time 2023 7 30)] + (testing "it returns :today when day equals today" + (is (= :today (utils/get-day-state today today year month start-date end-date)))) + (testing "it returns :selected when day equals start-date and not today" + (is + (= :selected (utils/get-day-state start-date today year month start-date end-date)))) + (testing "it returns :selected when day equals end-date and not today" + (is + (= :selected (utils/get-day-state end-date today year month start-date end-date)))))) + +(deftest update-range-test + (let [start-date (time/date-time 2023 7 20) + end-date (time/date-time 2023 7 30) + day (time/date-time 2023 7 27)] + (testing "it returns updated range" + (is + (= {:start-date day :end-date nil} (utils/update-range day start-date end-date)))))) + +(deftest in-range-test + (let [start-date (time/date-time 2023 7 20) + end-date (time/date-time 2023 7 30) + day (time/date-time 2023 7 27)] + (testing "it returns true when day is within range" + (is (utils/in-range? day start-date end-date)) + (is (not (utils/in-range? (time/date-time 2023 7 19) start-date end-date)))))) + +(deftest get-in-range-pos-test + (let [start-date (time/date-time 2023 7 20) + end-date (time/date-time 2023 7 30) + day (time/date-time 2023 7 27)] + (testing "it returns correct position within range" + (is (= :start (utils/get-in-range-pos start-date start-date end-date))) + (is (= :end (utils/get-in-range-pos end-date start-date end-date))) + (is (= :middle (utils/get-in-range-pos day start-date end-date)))))) diff --git a/src/quo2/components/calendar/calendar/days_grid/view.cljs b/src/quo2/components/calendar/calendar/days_grid/view.cljs new file mode 100644 index 00000000000..8226e90ac39 --- /dev/null +++ b/src/quo2/components/calendar/calendar/days_grid/view.cljs @@ -0,0 +1,41 @@ +(ns quo2.components.calendar.calendar.days-grid.view + (:require [react-native.core :as rn] + [cljs-time.core :as time] + [quo2.components.calendar.calendar.days-grid.utils :as utils] + [quo2.components.calendar.calendar-day.view :as calendar-day] + [quo2.components.calendar.calendar.days-grid.style :as style])) + +(defn- day-view + [day _ _ {:keys [year month selection-range on-press customization-color]}] + (let [today (time/now) + start-date (:start-date selection-range) + end-date (:end-date selection-range) + state (utils/get-day-state day today year month start-date end-date) + in-range (utils/get-in-range-pos day start-date end-date) + on-press #(on-press (time/date-time day))] + [calendar-day/view + {:customization-color customization-color + :state state + :in-range in-range + :on-press on-press} + (str (time/day day))])) + +(defn view + [{:keys [year month on-change start-date end-date customization-color]}] + (let [on-day-press (fn [day] + (let [new-selection (utils/update-range day start-date end-date)] + (on-change new-selection)))] + [rn/view + {:style style/container-days} + [rn/flat-list + {:data (utils/day-grid year month) + :key-fn str + :num-columns 7 + :content-container-style {:margin-horizontal -2} + :render-fn day-view + :render-data {:customization-color customization-color + :year year + :month month + :on-press on-day-press + :selection-range {:start-date start-date + :end-date end-date}}}]])) diff --git a/src/quo2/components/calendar/calendar/month_picker/component_spec.cljs b/src/quo2/components/calendar/calendar/month_picker/component_spec.cljs new file mode 100644 index 00000000000..e1652ae7b5a --- /dev/null +++ b/src/quo2/components/calendar/calendar/month_picker/component_spec.cljs @@ -0,0 +1,25 @@ +(ns quo2.components.calendar.calendar.month-picker.component-spec + (:require [quo2.components.calendar.calendar.month-picker.view :as month-picker] + [test-helpers.component :as h])) + +(h/describe "month-picker component" + (h/test "default render of month-picker component" + (h/render + [month-picker/view + {:year "2023" :month "7"}]) + (-> (h/expect (h/query-by-translation-text "July 2023")) + (h/is-truthy))) + + (h/test "should call on-change with next month when right button pressed" + (let [on-change (h/mock-fn)] + (h/render + [month-picker/view {:year "2023" :month "7" :on-change on-change}]) + (h/fire-event :press (h/query-by-label-text :next-month-button)) + (h/was-called on-change))) + + (h/test "should call on-change with previous month when left button pressed" + (let [on-change (h/mock-fn)] + (h/render + [month-picker/view {:year "2023" :month "1" :on-change on-change}]) + (h/fire-event :press (h/query-by-label-text :previous-month-button)) + (h/was-called on-change)))) diff --git a/src/quo2/components/calendar/calendar/month_picker/style.cljs b/src/quo2/components/calendar/calendar/month_picker/style.cljs new file mode 100644 index 00000000000..dc2edd75c44 --- /dev/null +++ b/src/quo2/components/calendar/calendar/month_picker/style.cljs @@ -0,0 +1,14 @@ +(ns quo2.components.calendar.calendar.month-picker.style + (:require [quo2.foundations.colors :as colors])) + +(def container + {:align-items :center + :flex-direction :row + :flex-grow 1 + :padding-horizontal 12 + :padding-vertical 9 + :justify-content :space-between}) + +(defn text + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme)}) diff --git a/src/quo2/components/calendar/calendar/month_picker/utils.cljs b/src/quo2/components/calendar/calendar/month_picker/utils.cljs new file mode 100644 index 00000000000..8b9cd351f11 --- /dev/null +++ b/src/quo2/components/calendar/calendar/month_picker/utils.cljs @@ -0,0 +1,22 @@ +(ns quo2.components.calendar.calendar.month-picker.utils + (:require [utils.datetime :as datetime])) + +(defn format-month-year + [year month] + (let [month (cond + (or (nil? month) (zero? month)) 1 + (> month 12) 12 + :else month)] + (str (datetime/format-long-month month) " " year))) + +(defn next-month + [year month] + (let [new-month (if (= month 12) 1 (inc month)) + new-year (if (= month 12) (inc year) year)] + {:year (str new-year) :month (str new-month)})) + +(defn previous-month + [year month] + (let [new-month (if (= month 1) 12 (dec month)) + new-year (if (= month 1) (dec year) year)] + {:year (str new-year) :month (str new-month)})) diff --git a/src/quo2/components/calendar/calendar/month_picker/utils_test.cljs b/src/quo2/components/calendar/calendar/month_picker/utils_test.cljs new file mode 100644 index 00000000000..4e416cd21d4 --- /dev/null +++ b/src/quo2/components/calendar/calendar/month_picker/utils_test.cljs @@ -0,0 +1,20 @@ +(ns quo2.components.calendar.calendar.month-picker.utils-test + (:require [cljs.test :refer-macros [deftest is testing]] + [quo2.components.calendar.calendar.month-picker.utils :as utils])) + +(deftest format-month-year-test + (testing "returns correct format for given year and month" + (is (= (utils/format-month-year 2023 1) "January 2023")) + (is (= (utils/format-month-year 2023 12) "December 2023")) + (is (= (utils/format-month-year 2023 0) "January 2023")) + (is (= (utils/format-month-year 2023 13) "December 2023")))) + +(deftest next-month-test + (testing "returns the next month and year" + (is (= (utils/next-month 2023 1) {:year "2023" :month "2"})) + (is (= (utils/next-month 2023 12) {:year "2024" :month "1"})))) + +(deftest previous-month-test + (testing "returns the previous month and year" + (is (= (utils/previous-month 2023 1) {:year "2022" :month "12"})) + (is (= (utils/previous-month 2023 12) {:year "2023" :month "11"})))) diff --git a/src/quo2/components/calendar/calendar/month_picker/view.cljs b/src/quo2/components/calendar/calendar/month_picker/view.cljs new file mode 100644 index 00000000000..fc7d7b69c65 --- /dev/null +++ b/src/quo2/components/calendar/calendar/month_picker/view.cljs @@ -0,0 +1,36 @@ +(ns quo2.components.calendar.calendar.month-picker.view + (:require [react-native.core :as rn] + [utils.number :as utils.number] + [quo2.theme :as theme] + [quo2.components.buttons.button.view :as button] + [quo2.components.markdown.text :as text] + [quo2.components.calendar.calendar.month-picker.style :as style] + [quo2.components.calendar.calendar.month-picker.utils :as utils])) + +(defn- view-internal + [{:keys [year month on-change theme]}] + (let [year (utils.number/parse-int year) + month (utils.number/parse-int month)] + [rn/view + {:style style/container} + [button/button + {:icon true + :type :outline + :accessibility-label :previous-month-button + :size 24 + :on-press #(on-change (utils/previous-month year month))} + :i/chevron-left] + [text/text + {:weight :semi-bold + :size :paragraph-1 + :style (style/text theme)} + (utils/format-month-year year month)] + [button/button + {:icon true + :accessibility-label :next-month-button + :size 24 + :type :outline + :on-press #(on-change (utils/next-month year month))} + :i/chevron-right]])) + +(def view (theme/with-theme view-internal)) diff --git a/src/quo2/components/calendar/calendar/style.cljs b/src/quo2/components/calendar/calendar/style.cljs new file mode 100644 index 00000000000..505464bdc37 --- /dev/null +++ b/src/quo2/components/calendar/calendar/style.cljs @@ -0,0 +1,14 @@ +(ns quo2.components.calendar.calendar.style + (:require [quo2.foundations.colors :as colors])) + +(defn container + [theme] + {:flex-direction :row + :height 270 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme) + :border-radius 12 + :border-width 1 + :background-color (colors/theme-colors colors/white colors/neutral-80-opa-40 theme)}) + +(def container-main + {:flex-grow 1}) diff --git a/src/quo2/components/calendar/calendar/utils.cljs b/src/quo2/components/calendar/calendar/utils.cljs new file mode 100644 index 00000000000..ae88cab2641 --- /dev/null +++ b/src/quo2/components/calendar/calendar/utils.cljs @@ -0,0 +1,25 @@ +(ns quo2.components.calendar.calendar.utils + (:require [utils.datetime :as datetime] + [utils.number :as utils.number] + [clojure.string :as string])) + +(defn generate-years + [current-year] + (let [current-year current-year] + (reverse (vec (range (- current-year 100) (+ current-year 1)))))) + +(defn current-year + [] + (-> (datetime/now) + datetime/timestamp->year-month-day-date + (string/split #"-") + first + utils.number/parse-int)) + +(defn current-month + [] + (-> (datetime/now) + datetime/timestamp->year-month-day-date + (string/split #"-") + second + utils.number/parse-int)) diff --git a/src/quo2/components/calendar/calendar/utils_test.cljs b/src/quo2/components/calendar/calendar/utils_test.cljs new file mode 100644 index 00000000000..e2febd0997c --- /dev/null +++ b/src/quo2/components/calendar/calendar/utils_test.cljs @@ -0,0 +1,30 @@ +(ns quo2.components.calendar.calendar.utils-test + (:require [cljs.test :refer-macros [deftest is testing]] + [quo2.components.calendar.calendar.utils :as utils] + [utils.datetime :as datetime] + [clojure.string :as string] + [utils.number :as utils.number])) + +(deftest generate-years-test + (testing "returns correct years range" + (let [current-year (utils/current-year)] + (is (= (last (utils/generate-years current-year)) (- current-year 100))) + (is (= (first (utils/generate-years current-year)) current-year))))) + +(deftest current-year-test + (testing "returns the current year" + (let [current-year (-> (datetime/now) + datetime/timestamp->year-month-day-date + (string/split #"-") + first + utils.number/parse-int)] + (is (= (utils/current-year) current-year))))) + +(deftest current-month-test + (testing "returns the current month" + (let [current-month (-> (datetime/now) + datetime/timestamp->year-month-day-date + (string/split #"-") + second + utils.number/parse-int)] + (is (= (utils/current-month) current-month))))) diff --git a/src/quo2/components/calendar/calendar/view.cljs b/src/quo2/components/calendar/calendar/view.cljs new file mode 100644 index 00000000000..0c1d3d08d06 --- /dev/null +++ b/src/quo2/components/calendar/calendar/view.cljs @@ -0,0 +1,42 @@ +(ns quo2.components.calendar.calendar.view + (:require [react-native.core :as rn] + [quo2.theme :as theme] + [reagent.core :as reagent] + [utils.number :as utils.number] + [quo2.components.calendar.calendar.utils :as utils] + [quo2.components.calendar.calendar.style :as style] + [quo2.components.calendar.calendar.years-list.view :as years-list] + [quo2.components.calendar.calendar.days-grid.view :as days-grid] + [quo2.components.calendar.calendar.weekdays-header.view :as weekdays-header] + [quo2.components.calendar.calendar.month-picker.view :as month-picker])) + +(defn- view-internal + [] + (let [selected-year (reagent/atom (utils/current-year)) + selected-month (reagent/atom (utils/current-month)) + on-change-year #(reset! selected-year %) + on-change-month (fn [new-date] + (reset! selected-year (utils.number/parse-int (:year new-date))) + (reset! selected-month (utils.number/parse-int (:month new-date))))] + (fn [{:keys [on-change start-date end-date theme]}] + [rn/view + {:style (style/container theme)} + [years-list/view + {:on-change-year on-change-year + :year @selected-year}] + [rn/view + {:style style/container-main} + [month-picker/view + {:year @selected-year + :month @selected-month + :on-change on-change-month}] + [weekdays-header/view] + [days-grid/view + {:year @selected-year + :month @selected-month + :start-date start-date + :end-date end-date + :on-change on-change + :customization-color :blue}]]]))) + +(def view (theme/with-theme view-internal)) diff --git a/src/quo2/components/calendar/calendar/weekdays_header/style.cljs b/src/quo2/components/calendar/calendar/weekdays_header/style.cljs new file mode 100644 index 00000000000..044328e097d --- /dev/null +++ b/src/quo2/components/calendar/calendar/weekdays_header/style.cljs @@ -0,0 +1,18 @@ +(ns quo2.components.calendar.calendar.weekdays-header.style + (:require [quo2.foundations.colors :as colors])) + +(def container-weekday-row + {:flex-direction :row + :justify-content :space-between + :padding-horizontal 8}) + +(def container-weekday + {:width 32 + :height 30 + :padding-top 2 + :justify-content :center + :align-items :center}) + +(defn text-weekdays + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) diff --git a/src/quo2/components/calendar/calendar/weekdays_header/view.cljs b/src/quo2/components/calendar/calendar/weekdays_header/view.cljs new file mode 100644 index 00000000000..233f1305577 --- /dev/null +++ b/src/quo2/components/calendar/calendar/weekdays_header/view.cljs @@ -0,0 +1,23 @@ +(ns quo2.components.calendar.calendar.weekdays-header.view + (:require [react-native.core :as rn] + [quo2.theme :as theme] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [quo2.components.markdown.text :as text] + [quo2.components.calendar.calendar.weekdays-header.style :as style])) + +(defn- view-internal + [theme] + [rn/view + {:style style/container-weekday-row} + (for [weekday datetime/weekday-names] + [rn/view + {:style style/container-weekday + :key weekday} + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/text-weekdays theme)} + (str (i18n/label weekday))]])]) + +(def view (theme/with-theme view-internal)) diff --git a/src/quo2/components/calendar/calendar/years_list/style.cljs b/src/quo2/components/calendar/calendar/years_list/style.cljs new file mode 100644 index 00000000000..6930acca9d6 --- /dev/null +++ b/src/quo2/components/calendar/calendar/years_list/style.cljs @@ -0,0 +1,33 @@ +(ns quo2.components.calendar.calendar.years-list.style + (:require [quo2.foundations.colors :as colors])) + +(defn gradient-start-color + [theme] + (colors/theme-colors colors/white colors/neutral-90 theme)) + +(defn gradient-end-color + [theme] + (colors/theme-colors colors/white-opa-0 colors/neutral-100-opa-0 theme)) + +(def gradient-view + {:position :absolute + :height 50 + :border-top-left-radius 12 + :top 0 + :left 0 + :right 0}) + +(defn container-years + [theme] + {:border-width 1 + :overflow :hidden + :padding-left 8 + :padding-right 7 + :padding-vertical 8 + :margin-left -1 + :margin-top -1 + :margin-bottom -1 + :border-style :dashed + :border-top-left-radius 12 + :border-bottom-left-radius 12 + :border-color (colors/theme-colors colors/neutral-20 colors/neutral-80 theme)}) diff --git a/src/quo2/components/calendar/calendar/years_list/view.cljs b/src/quo2/components/calendar/calendar/years_list/view.cljs new file mode 100644 index 00000000000..33561134688 --- /dev/null +++ b/src/quo2/components/calendar/calendar/years_list/view.cljs @@ -0,0 +1,49 @@ +(ns quo2.components.calendar.calendar.years-list.view + (:require [react-native.core :as rn] + [quo2.theme :as theme] + [react-native.linear-gradient :as linear-gradient] + [quo2.components.calendar.calendar.utils :as utils] + [quo2.components.calendar.calendar-year.view :as calendar-year] + [quo2.components.calendar.calendar.years-list.style :as style])) + +(defn- year-view + [year _ _ {:keys [selected-year on-press]}] + [calendar-year/view + {:selected? (= year selected-year) + :on-press #(on-press year)} + (str year)]) + +(defn- separator + [] + [rn/view {:style {:height 4}}]) + +(defn- footer + [] + [rn/view {:style {:height 32}}]) + +(defn- gradiant-overview + [theme] + [linear-gradient/linear-gradient + {:colors [(style/gradient-start-color theme) (style/gradient-end-color theme)] + :style style/gradient-view + :start {:x 0 :y 0} + :end {:x 0 :y 1}}]) + +(defn view-internal + [{:keys [on-change-year year theme]}] + [rn/view + {:style (style/container-years theme)} + [rn/flat-list + {:data (utils/generate-years (utils/current-year)) + :key-fn str + :list-key :years-list + :inverted true + :shows-vertical-scroll-indicator false + :footer [footer] + :separator [separator] + :render-fn year-view + :render-data {:selected-year year + :on-press #(on-change-year %)}}] + [gradiant-overview theme]]) + +(def view (theme/with-theme view-internal)) diff --git a/src/quo2/components/calendar/calendar_day/component_spec.cljs b/src/quo2/components/calendar/calendar_day/component_spec.cljs new file mode 100644 index 00000000000..e4feccfe7b3 --- /dev/null +++ b/src/quo2/components/calendar/calendar_day/component_spec.cljs @@ -0,0 +1,14 @@ +(ns quo2.components.calendar.calendar-day.component-spec + (:require [quo2.components.calendar.calendar-day.view :as calendar-day] + [test-helpers.component :as h])) + +(h/describe "calendar-day component" + (h/test "default render of calendar-day component" + (h/render [calendar-day/view {} "25"]) + (h/is-truthy (h/query-by-text "25"))) + + (h/test "should not call on-press when state is :disabled" + (let [on-press (h/mock-fn)] + (h/render [calendar-day/view {:on-press on-press :state :disabled} "25"]) + (h/fire-event :press (h/query-by-text "25")) + (h/was-not-called on-press)))) diff --git a/src/quo2/components/calendar/calendar_day/style.cljs b/src/quo2/components/calendar/calendar_day/style.cljs new file mode 100644 index 00000000000..65592e09874 --- /dev/null +++ b/src/quo2/components/calendar/calendar_day/style.cljs @@ -0,0 +1,69 @@ +(ns quo2.components.calendar.calendar-day.style + (:require [quo2.foundations.colors :as colors])) + +(def wrapper + {:flex 1 + :margin-vertical 2 + :justify-content :center + :align-items :center}) + +(def container-base + {:align-items :center + :justify-content :center + :border-radius 10 + :height 32 + :width 32}) + +(defn text-base + [theme] + {:color (colors/theme-colors colors/neutral-100 colors/white theme) + :text-align :center}) + +(defn in-range-background + [{:keys [in-range theme]}] + (cond-> {:position :absolute + :top 0 + :right 0 + :left 0 + :bottom 0} + (= in-range :start) + (assoc :background-color + (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :left 20) + + (= in-range :middle) + (assoc :background-color + (colors/theme-colors colors/neutral-5 colors/neutral-80 theme)) + + (= in-range :end) + (assoc :background-color + (colors/theme-colors colors/neutral-5 colors/neutral-80 theme) + :right 20))) + +(defn container + [{:keys [state theme customization-color]}] + (cond-> container-base + (= state :default) + (assoc :background-color colors/neutral-100-opa-0) + + (= state :disabled) + (assoc :opacity 0.3) + + (= state :selected) + (assoc :background-color (colors/custom-color-by-theme customization-color 50 60 nil nil theme)))) + +(defn text + [{:keys [state theme]}] + (cond-> (text-base theme) + (= state :selected) (assoc :color colors/white))) + +(defn indicator + [{:keys [state theme customization-color]}] + {:width 4 + :position :absolute + :bottom 3 + :height 2 + :border-radius 8 + :background-color (if (= state :today) + (colors/custom-color-by-theme customization-color 50 60 nil nil theme) + colors/neutral-100-opa-0)}) diff --git a/src/quo2/components/calendar/calendar_day/view.cljs b/src/quo2/components/calendar/calendar_day/view.cljs new file mode 100644 index 00000000000..4e050bd550c --- /dev/null +++ b/src/quo2/components/calendar/calendar_day/view.cljs @@ -0,0 +1,31 @@ +(ns quo2.components.calendar.calendar-day.view + (:require [react-native.core :as rn] + [quo2.theme :as theme] + [quo2.components.markdown.text :as text] + [quo2.components.calendar.calendar-day.style :as style])) + +(defn- view-internal + [{:keys [state in-range on-press customization-color theme] + :or {state :default}} + day] + [rn/view {:style style/wrapper} + [rn/view {:style (style/in-range-background {:in-range in-range :theme theme})}] + [rn/touchable-opacity + {:on-press on-press + :style (style/container + {:state state + :theme theme + :customization-color customization-color}) + :disabled (= state :disabled)} + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/text {:state state :theme theme})} + day] + [rn/view + {:style (style/indicator + {:state state + :theme theme + :customization-color customization-color})}]]]) + +(def view (theme/with-theme view-internal)) diff --git a/src/quo2/components/calendar/calendar_year/component_spec.cljs b/src/quo2/components/calendar/calendar_year/component_spec.cljs new file mode 100644 index 00000000000..b38db6e9a5a --- /dev/null +++ b/src/quo2/components/calendar/calendar_year/component_spec.cljs @@ -0,0 +1,14 @@ +(ns quo2.components.calendar.calendar-year.component-spec + (:require [quo2.components.calendar.calendar-year.view :as calendar-year] + [test-helpers.component :as h])) + +(h/describe "calendar-year component" + (h/test "default render of calendar-year component" + (h/render [calendar-year/view {} "2023"]) + (h/is-truthy (h/query-by-text "2023"))) + + (h/test "should not call on-press when disabled" + (let [on-press (h/mock-fn)] + (h/render [calendar-year/view {:on-press on-press :disabled? true} "2023"]) + (h/fire-event :press (h/query-by-text "2023")) + (h/was-not-called on-press)))) diff --git a/src/quo2/components/calendar/calendar_year/style.cljs b/src/quo2/components/calendar/calendar_year/style.cljs new file mode 100644 index 00000000000..78d5b048d6f --- /dev/null +++ b/src/quo2/components/calendar/calendar_year/style.cljs @@ -0,0 +1,25 @@ +(ns quo2.components.calendar.calendar-year.style + (:require [quo2.foundations.colors :as colors])) + +(def container-base + {:align-items :center + :justify-content :center + :border-radius 10 + :height 32 + :width 48}) + +(defn text-base + [theme] + {:color (colors/theme-colors colors/neutral-50 colors/neutral-40 theme)}) + +(defn container + [{:keys [selected? disabled? theme]}] + (cond-> container-base + disabled? (assoc :opacity 0.3) + selected? (assoc :background-color + (colors/theme-colors colors/neutral-10 colors/neutral-70 theme)))) + +(defn text + [{:keys [selected? theme]}] + (cond-> (text-base theme) + selected? (assoc :color (colors/theme-colors colors/neutral-100 colors/white theme)))) diff --git a/src/quo2/components/calendar/calendar_year/view.cljs b/src/quo2/components/calendar/calendar_year/view.cljs new file mode 100644 index 00000000000..f4787dffba6 --- /dev/null +++ b/src/quo2/components/calendar/calendar_year/view.cljs @@ -0,0 +1,24 @@ +(ns quo2.components.calendar.calendar-year.view + (:require [react-native.core :as rn] + [quo2.theme :as theme] + [quo2.components.markdown.text :as text] + [quo2.components.calendar.calendar-year.style :as style])) + +(defn- view-internal + [{:keys [selected? disabled? on-press theme]} year] + [rn/touchable-opacity + {:on-press on-press + :style (style/container + {:selected? selected? + :disabled? disabled? + :theme theme}) + :disabled disabled?} + [text/text + {:weight :medium + :size :paragraph-2 + :style (style/text + {:selected? selected? + :theme theme})} + year]]) + +(def view (theme/with-theme view-internal)) diff --git a/src/quo2/core.cljs b/src/quo2/core.cljs index fd8e865a664..8933d1534d3 100644 --- a/src/quo2/core.cljs +++ b/src/quo2/core.cljs @@ -13,6 +13,9 @@ quo2.components.buttons.predictive-keyboard.view quo2.components.buttons.slide-button.view quo2.components.browser.browser-input.view + quo2.components.calendar.calendar.view + quo2.components.calendar.calendar-day.view + quo2.components.calendar.calendar-year.view quo2.components.code.snippet quo2.components.colors.color-picker.view quo2.components.common.separator.view @@ -120,6 +123,11 @@ ;;;; BROWSER (def browser-input quo2.components.browser.browser-input.view/browser-input) +;;;; CALENDAR +(def calendar quo2.components.calendar.calendar.view/view) +(def calendar-day quo2.components.calendar.calendar-day.view/view) +(def calendar-year quo2.components.calendar.calendar-year.view/view) + ;;;; CODE (def snippet quo2.components.code.snippet/snippet) diff --git a/src/quo2/core_spec.cljs b/src/quo2/core_spec.cljs index a4dc811ab34..4e1232e6483 100644 --- a/src/quo2/core_spec.cljs +++ b/src/quo2/core_spec.cljs @@ -5,6 +5,10 @@ [quo2.components.buttons.button.component-spec] [quo2.components.buttons.predictive-keyboard.component-spec] [quo2.components.buttons.slide-button.component-spec] + [quo2.components.calendar.calendar.component-spec] + [quo2.components.calendar.calendar-day.component-spec] + [quo2.components.calendar.calendar.month-picker.component-spec] + [quo2.components.calendar.calendar-year.component-spec] [quo2.components.browser.browser-input.component-spec] [quo2.components.colors.color-picker.component-spec] [quo2.components.counter.component-spec] diff --git a/src/status_im2/contexts/quo_preview/calendar/calendar.cljs b/src/status_im2/contexts/quo_preview/calendar/calendar.cljs new file mode 100644 index 00000000000..e7dd0fcf2d7 --- /dev/null +++ b/src/status_im2/contexts/quo_preview/calendar/calendar.cljs @@ -0,0 +1,42 @@ +(ns status-im2.contexts.quo-preview.calendar.calendar + (:require [status-im2.contexts.quo-preview.preview :as preview] + [react-native.core :as rn] + [utils.datetime :as dt] + [quo2.foundations.colors :as colors] + [reagent.core :as reagent] + [quo2.core :as quo])) + +(def descriptor + [{:label "Start Date" + :key :start-date + :type :text} + {:label "End Date" + :key :end-date + :type :text}]) + +(defn cool-preview + [] + (let [state (reagent/atom {:start-date nil :end-date nil}) + range (reagent/atom {:start-date nil :end-date nil})] + (fn + [] + [rn/touchable-without-feedback + {:on-press rn/dismiss-keyboard!} + [rn/view {:style {:flex 1}} + [preview/customizer state descriptor] + [rn/view {:style {:padding 19 :flex-grow 2}} + [quo/calendar + {:start-date (:start-date @range) + :end-date (:end-date @range) + :on-change (fn [new-range] + (reset! state + {:start-date (dt/format-date (:start-date new-range)) + :end-date (dt/format-date (:end-date new-range))}) + (reset! range new-range))}]]]]))) + +(defn preview-calendar + [] + [rn/view + {:style {:background-color (colors/theme-colors colors/white colors/neutral-95) + :flex 1}} + [cool-preview]]) diff --git a/src/status_im2/contexts/quo_preview/calendar/calendar_day.cljs b/src/status_im2/contexts/quo_preview/calendar/calendar_day.cljs new file mode 100644 index 00000000000..6de205af630 --- /dev/null +++ b/src/status_im2/contexts/quo_preview/calendar/calendar_day.cljs @@ -0,0 +1,45 @@ +(ns status-im2.contexts.quo-preview.calendar.calendar-day + (:require [status-im2.contexts.quo-preview.preview :as preview] + [react-native.core :as rn] + [quo2.foundations.colors :as colors] + [reagent.core :as reagent] + [quo2.core :as quo])) + +(def descriptor + [{:label "State:" + :key :state + :type :select + :options [{:key :default + :value "Default"} + {:key :selected + :value "Selected"} + {:key :disabled + :value "Disabled"} + {:key :today + :value "Today"}]}]) + +(defn cool-preview + [] + (let [state (reagent/atom + {:state :default})] + (fn + [] + [rn/touchable-without-feedback + {:on-press rn/dismiss-keyboard!} + [rn/view + [preview/customizer state descriptor] + [rn/view + {:padding-vertical 60 + :align-items :center} + [quo/calendar-day (assoc @state :customization-color :blue) 12]]]]))) + +(defn preview-calendar-day + [] + [rn/view + {:style {:background-color (colors/theme-colors colors/white colors/neutral-95) + :flex 1}} + [rn/flat-list + {:style {:flex 1} + :keyboard-should-persist-taps :always + :header [cool-preview] + :key-fn str}]]) diff --git a/src/status_im2/contexts/quo_preview/calendar/calendar_year.cljs b/src/status_im2/contexts/quo_preview/calendar/calendar_year.cljs new file mode 100644 index 00000000000..4b291a093e5 --- /dev/null +++ b/src/status_im2/contexts/quo_preview/calendar/calendar_year.cljs @@ -0,0 +1,39 @@ +(ns status-im2.contexts.quo-preview.calendar.calendar-year + (:require [status-im2.contexts.quo-preview.preview :as preview] + [react-native.core :as rn] + [quo2.foundations.colors :as colors] + [reagent.core :as reagent] + [quo2.core :as quo])) + +(def descriptor + [{:label "Selected?" + :key :selected? + :type :boolean} + {:label "Disabled?" + :key :disabled? + :type :boolean}]) + +(defn cool-preview + [] + (let [state (reagent/atom {:selected? false :disabled? false})] + (fn + [] + [rn/touchable-without-feedback + {:on-press rn/dismiss-keyboard!} + [rn/view + [preview/customizer state descriptor] + [rn/view + {:padding-vertical 60 + :align-items :center} + [quo/calendar-year @state "2023"]]]]))) + +(defn preview-calendar-year + [] + [rn/view + {:style {:background-color (colors/theme-colors colors/white colors/neutral-95) + :flex 1}} + [rn/flat-list + {:style {:flex 1} + :keyboard-should-persist-taps :always + :header [cool-preview] + :key-fn str}]]) diff --git a/src/status_im2/contexts/quo_preview/main.cljs b/src/status_im2/contexts/quo_preview/main.cljs index d493e652c6e..75110869df0 100644 --- a/src/status_im2/contexts/quo_preview/main.cljs +++ b/src/status_im2/contexts/quo_preview/main.cljs @@ -19,6 +19,9 @@ [status-im2.contexts.quo-preview.buttons.slide-button :as slide-button] [status-im2.contexts.quo-preview.buttons.dynamic-button :as dynamic-button] [status-im2.contexts.quo-preview.buttons.predictive-keyboard :as predictive-keyboard] + [status-im2.contexts.quo-preview.calendar.calendar :as calendar] + [status-im2.contexts.quo-preview.calendar.calendar-day :as calendar-day] + [status-im2.contexts.quo-preview.calendar.calendar-year :as calendar-year] [status-im2.contexts.quo-preview.browser.browser-input :as browser-input] [status-im2.contexts.quo-preview.code.snippet :as code-snippet] [status-im2.contexts.quo-preview.colors.color-picker :as color-picker] @@ -145,6 +148,15 @@ :browser [{:name :browser-input :options {:topBar {:visible false}} :component browser-input/preview-browser-input}] + :calendar [{:name :calendar + :options {:topBar {:visible true}} + :component calendar/preview-calendar} + {:name :calendar-day + :options {:topBar {:visible true}} + :component calendar-day/preview-calendar-day} + {:name :calendar-year + :options {:topBar {:visible true}} + :component calendar-year/preview-calendar-year}] :code [{:name :snippet :options {:topBar {:visible true}} :component code-snippet/preview-code-snippet}] diff --git a/src/test_helpers/component.cljs b/src/test_helpers/component.cljs index e4f719b2206..ba44cf1af85 100644 --- a/src/test_helpers/component.cljs +++ b/src/test_helpers/component.cljs @@ -204,6 +204,10 @@ [mock] (.toHaveBeenCalled (js/expect mock))) +(defn was-called-with + [mock expected-arg] + (.toHaveBeenCalledWith (js/expect mock) expected-arg)) + (defn was-called-times [^js mock number-of-times] (.toHaveBeenCalledTimes (js/expect mock) number-of-times)) diff --git a/src/utils/datetime.cljs b/src/utils/datetime.cljs index b0da527c49f..6efc99e3624 100644 --- a/src/utils/datetime.cljs +++ b/src/utils/datetime.cljs @@ -9,6 +9,8 @@ (defn now [] (t/now)) +(def weekday-names ["su" "mo" "tu" "we" "th" "fr" "sa"]) + (def ^:const int->weekday "Maps the corresponding string representation of a weekday By it's numeric index as in cljs-time" @@ -93,6 +95,12 @@ [^js locsym] (nth (.-DATEFORMATS locsym) 2)) +(defn format-date + [date] + (if date + (t.format/unparse (t.format/formatter "dd/MM/yyyy") date) + "")) + ;;;; Datetime formats (defn- medium-date-time-format [locsym] @@ -113,6 +121,10 @@ (def short-date-with-time-fmt (get-formatter-fn short-date-format-with-time)) (def datetime-within-one-week-fmt (get-formatter-fn datetime-within-one-week-format)) +(def format-long-month + (memoize (fn [month] + (.format ^js ((get-formatter-fn (constantly "MMMM"))) + (t/date-time 1970 month))))) ;;;; Utilities (defn previous-years? [datetime] diff --git a/src/utils/datetime_test.cljs b/src/utils/datetime_test.cljs index ca4a9fe1a0e..aa013459bc4 100644 --- a/src/utils/datetime_test.cljs +++ b/src/utils/datetime_test.cljs @@ -190,3 +190,11 @@ "it" #'utils.datetime/medium-date-time-format)] (is (= (datetime/day-relative epoch) "01 gen 1970, 12:00:00 AM"))))) + +(deftest format-long-month-test + (testing "returns correct month string" + (is (= "January" (datetime/format-long-month 1))) + (is (= "February" (datetime/format-long-month 2))) + (is (= "March" (datetime/format-long-month 3))) + (is (= "April" (datetime/format-long-month 4))) + (is (= "December" (datetime/format-long-month 12))))) diff --git a/translations/en.json b/translations/en.json index a4802911e53..9aefc8b1ea1 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2221,6 +2221,13 @@ "all-messages": "All messages", "muted-until": "Muted until {{duration}}", "until-you-turn-it-back-on": "you turn it back on", + "mo": "Mo", + "tu": "Tu", + "we": "We", + "th": "Th", + "fr": "Fr", + "sa": "Sa", + "su": "Su", "mon": "Mon", "tue": "Tue", "wed": "Wed",