├── bin
├── open-prod-db.sh
├── postgres-heroku.sh
├── start-dev.sh
├── open-local-db.sh
├── postgres-local.sh
├── setup
├── heroku-logs.sh
├── deploy.sh
├── run-server-jar.sh
├── make-jar.sh
└── set-env-variables TEMPLATE.sh
├── dev
├── design mocks
│ ├── about.png
│ ├── main screen.jpg
│ └── main screen map.jpg
└── dev
│ └── repl.clj
├── resources
├── public
│ ├── washi.png
│ ├── az-subtle.png
│ ├── devons_map.png
│ ├── kiwi-favicon.png
│ ├── orange-favicon.png
│ ├── meetcute-opengraph.png
│ ├── oranges-tile-repeat.jpg
│ ├── oranges-tile-repeat-2.jpg
│ ├── signup.js
│ ├── signin.js
│ ├── heroku-errors
│ │ └── application-error.html
│ ├── meetcute.html
│ ├── index.html
│ ├── privacy-policy.html
│ ├── terms-and-conditions.html
│ └── css
│ │ └── meetcute.css
└── sql
│ ├── schema-impersonation.sql
│ ├── schema-friends.sql
│ ├── schema-coordinates.sql
│ ├── schema-access-tokens.sql
│ ├── schema-users.sql
│ ├── schema-twitter-profiles.sql
│ └── schema-settings.sql
├── Small World launch materials
├── Procfile
├── src
├── ketchup
│ ├── env.clj
│ ├── auth.clj
│ ├── notify.clj
│ ├── db.clj
│ └── routes.clj
├── meetcute
│ ├── screens
│ │ └── styles.cljc
│ ├── env.clj
│ ├── sms.clj
│ ├── auth_test.clj
│ ├── util.cljc
│ └── routes.clj
├── smallworld
│ ├── session.cljc
│ ├── util.clj
│ ├── email.clj
│ ├── memoize.clj
│ ├── admin.cljc
│ ├── util.cljs
│ ├── airtable.clj
│ ├── anki_pkg.clj
│ ├── coordinates.clj
│ ├── mocks.cljc
│ ├── frontend.cljs
│ ├── decorations.cljs
│ ├── user_data.cljs
│ ├── clj_postgresql
│ │ └── types.clj
│ ├── db.clj
│ ├── user_data.clj
│ └── screens
│ │ └── home.cljs
└── sdk
│ └── expo.clj
├── .vscode
├── settings.json
└── extensions.json
├── .gitignore
└── project.clj
/bin/open-prod-db.sh:
--------------------------------------------------------------------------------
1 | heroku pg:psql postgresql-fluffy-56995 --app small-world-friends
--------------------------------------------------------------------------------
/bin/postgres-heroku.sh:
--------------------------------------------------------------------------------
1 | heroku pg:psql postgresql-fluffy-56995 --app small-world-friends
--------------------------------------------------------------------------------
/bin/start-dev.sh:
--------------------------------------------------------------------------------
1 | source bin/set-env-variables.sh &&
2 | lein clean &&
3 | lein repl
--------------------------------------------------------------------------------
/bin/open-local-db.sh:
--------------------------------------------------------------------------------
1 | /Applications/Postgres.app/Contents/Versions/14/bin/psql -p5432 "devonzuegel"
--------------------------------------------------------------------------------
/bin/postgres-local.sh:
--------------------------------------------------------------------------------
1 | /Applications/Postgres.app/Contents/Versions/14/bin/psql -p5432 "devonzuegel"
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | psql -U postgres -c "CREATE DATABASE \"smallworld-local\""
4 |
--------------------------------------------------------------------------------
/dev/design mocks/about.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/dev/design mocks/about.png
--------------------------------------------------------------------------------
/resources/public/washi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/resources/public/washi.png
--------------------------------------------------------------------------------
/Small World launch materials:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/Small World launch materials
--------------------------------------------------------------------------------
/resources/public/az-subtle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/resources/public/az-subtle.png
--------------------------------------------------------------------------------
/dev/design mocks/main screen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/dev/design mocks/main screen.jpg
--------------------------------------------------------------------------------
/resources/public/devons_map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/resources/public/devons_map.png
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: java -Xmx300M -Xss512k -XX:CICompilerCount=2 -Dfile.encoding=UTF-8 -jar target/smallworld.jar -m smallworld.web
--------------------------------------------------------------------------------
/resources/public/kiwi-favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/resources/public/kiwi-favicon.png
--------------------------------------------------------------------------------
/resources/public/orange-favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/resources/public/orange-favicon.png
--------------------------------------------------------------------------------
/dev/design mocks/main screen map.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/dev/design mocks/main screen map.jpg
--------------------------------------------------------------------------------
/resources/public/meetcute-opengraph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/resources/public/meetcute-opengraph.png
--------------------------------------------------------------------------------
/resources/public/oranges-tile-repeat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/resources/public/oranges-tile-repeat.jpg
--------------------------------------------------------------------------------
/resources/public/oranges-tile-repeat-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devonzuegel/smallworld/HEAD/resources/public/oranges-tile-repeat-2.jpg
--------------------------------------------------------------------------------
/src/ketchup/env.clj:
--------------------------------------------------------------------------------
1 | (ns ketchup.env)
2 |
3 | (defn get-env-var [key]
4 | (let [value (System/getenv key)]
5 | (when (nil? value) (throw (Throwable. (str "Environment variable not set: " key))))
6 | value))
--------------------------------------------------------------------------------
/src/meetcute/screens/styles.cljc:
--------------------------------------------------------------------------------
1 | (ns meetcute.screens.styles)
2 |
3 | (def btn
4 | {:border "3px solid #cccccc33"
5 | :padding "12px"
6 | :border-radius "8px"
7 | :cursor "pointer"
8 | :margin "6px"})
--------------------------------------------------------------------------------
/src/meetcute/env.clj:
--------------------------------------------------------------------------------
1 | (ns meetcute.env)
2 |
3 | (defn get-env-var [key]
4 | (let [value (System/getenv key)]
5 | (assert (some? value)
6 | (str "Environment variable not set: " key))
7 | value))
8 |
9 |
--------------------------------------------------------------------------------
/src/smallworld/session.cljc:
--------------------------------------------------------------------------------
1 | (ns smallworld.session #?(:cljs (:require [reagent.core :as r])))
2 |
3 | (def blank {})
4 |
5 | #?(:cljs
6 | (do
7 | (defonce *store (r/atom :loading)) ; TODO: add (^:private) later
8 |
9 | (defn update! [new-session-data] (reset! *store new-session-data))))
--------------------------------------------------------------------------------
/bin/heroku-logs.sh:
--------------------------------------------------------------------------------
1 | echo ""
2 | echo "--------------------------------------------------"
3 | echo " starting up the heroku logs"
4 | echo "--------------------------------------------------"
5 |
6 | echo ""
7 | echo "running command:"
8 | echo " heroku logs --tail"
9 | echo ""
10 | heroku logs --tail
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.iconTheme": "city-lights-icons-vsc-light",
3 | "editor.formatOnSave": true,
4 | "workbench.colorTheme": "Winter is Coming (Dark Blue)",
5 | "workbench.preferredLightColorTheme": "Horizon Bright",
6 | "window.autoDetectColorScheme": true,
7 | "workbench.preferredDarkColorTheme": "Winter is Coming (Dark Blue)",
8 | "audioCues.enabled": "off"
9 | }
--------------------------------------------------------------------------------
/bin/deploy.sh:
--------------------------------------------------------------------------------
1 | echo ""
2 | echo "--------------------------------------------------"
3 | echo " deploying smallworld to heroku"
4 | echo "--------------------------------------------------"
5 |
6 | echo ""
7 | echo "running command:"
8 | echo " git push heroku HEAD:master # push the current branch to Heroku, whatever it’s called"
9 | echo ""
10 | git push heroku HEAD:master # push the current branch to Heroku, whatever it’s called
11 |
12 | bin/heroku-logs.sh
13 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
4 |
5 | // List of extensions which should be recommended for users of this workspace.
6 | "recommendations": [
7 |
8 | ],
9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
10 | "unwantedRecommendations": [
11 |
12 | ]
13 | }
--------------------------------------------------------------------------------
/bin/run-server-jar.sh:
--------------------------------------------------------------------------------
1 | echo ""
2 | echo "--------------------------------------------------"
3 | echo " starting the smallworld server"
4 | echo "--------------------------------------------------"
5 |
6 | echo ""
7 | echo "running command:"
8 | echo " source bin/set-env-variables.sh"
9 | echo ""
10 | source bin/set-env-variables.sh
11 |
12 | echo "running command:"
13 | echo " java -jar target/smallworld.jar -m smallworld.web"
14 | echo ""
15 | java -jar target/smallworld.jar -m smallworld.web
--------------------------------------------------------------------------------
/bin/make-jar.sh:
--------------------------------------------------------------------------------
1 | echo ""
2 | echo "--------------------------------------------------"
3 | echo " making the smallworld jar"
4 | echo "--------------------------------------------------"
5 |
6 | echo ""
7 | echo "running command:"
8 | echo " source bin/set-env-variables.sh"
9 | source bin/set-env-variables.sh
10 |
11 | echo ""
12 | echo "running command:"
13 | echo " lein clean"
14 | lein clean
15 |
16 | echo ""
17 | echo "running command:"
18 | echo " lein uberjar"
19 | echo ""
20 | lein uberjar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pom.xml
2 | *jar
3 | /classes/
4 | /.lein-deps-sum
5 | /.lein-failures
6 | /.lein-env
7 | /checkouts
8 | /.env
9 | /target
10 | !/target/smallworld.jar
11 | /.lein-repl-history
12 | /.nrepl-port
13 | .clj-kondo/
14 | .lsp/
15 | .DS_Store
16 | dev/.DS_Store
17 | .calva/
18 | figwheel_server.log
19 | .rebel_readline_history
20 | resources/public/js
21 | bin/set-env-variables.sh
22 |
23 | # IntelliJ files
24 | .idea/*
25 | *.iml
26 |
27 | # Database dumps (contain sensitive data)
28 | *.dump
29 | latest.dump
30 |
--------------------------------------------------------------------------------
/resources/public/signup.js:
--------------------------------------------------------------------------------
1 | // Helper to resize Airtable iframe and present a loading message for it
2 | const airtableIframe = document.getElementById('airtable-signup');
3 | const loadingSpinner = document.getElementById('loading-spinner');
4 |
5 | function onLoad() {
6 | loadingSpinner.style.display = 'none';
7 | airtableIframe.style.display = 'block';
8 | };
9 |
10 | if (airtableIframe) {
11 | const domHeight = window.innerHeight;
12 | const iframeHeight = domHeight - 180;
13 | airtableIframe.height = iframeHeight + 'px';
14 | airtableIframe.onload = onLoad;
15 | }
16 |
--------------------------------------------------------------------------------
/resources/public/signin.js:
--------------------------------------------------------------------------------
1 |
2 | // Manages the phone numbre input
3 | const displayPhoneInput = document.getElementById("display-phone");
4 | const hiddenPhoneInput = document.getElementById("phone");
5 |
6 | console.log('phone input', displayPhoneInput, hiddenPhoneInput);
7 |
8 | if (displayPhoneInput && hiddenPhoneInput) {
9 | const iti = window.intlTelInput(displayPhoneInput, {
10 | separateDialCode: true,
11 | initialCountry: "us",
12 | utilsScript:
13 | "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js",
14 | });
15 |
16 | displayPhoneInput.addEventListener('change', function(event) {
17 | var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.E164);
18 | hiddenPhoneInput.value = formattedNumber;
19 | });
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/dev/dev/repl.clj:
--------------------------------------------------------------------------------
1 | ;; this is nested under dev/dev/ to avoid namespace collisions. I know
2 | ;; it looks messy, but there's a reson for it!
3 |
4 | (ns dev.repl
5 | (:require [figwheel-sidecar.repl-api :as repl-api]
6 | [smallworld.web :as backend]))
7 |
8 | (def PORT 3001)
9 |
10 | #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
11 | (defn restart-server []
12 | (backend/stop!)
13 | (use 'smallworld.web :reload)
14 | (println "\nrestarting server......\n")
15 | (backend/-main))
16 |
17 | (defn initialize-repl []
18 | (println (str "\n\n🌎 starting the small world server (backend): http://localhost:" PORT " 🌍\n"))
19 | (backend/-main)
20 |
21 | (println "\n\n🎨 starting the Figwheel server (frontend hot-reloading) 🎨\n")
22 | (repl-api/start-figwheel!)
23 |
24 | (println (str "\n\n⚙️ (" 'restart-server ") – run this every time you update server-side code ⚙️"))
25 | (println "\n"))
26 |
27 | (initialize-repl)
--------------------------------------------------------------------------------
/resources/sql/schema-impersonation.sql:
--------------------------------------------------------------------------------
1 | -- This is for Small World, not for Ketchup Club
2 |
3 | create table if not exists impersonation (
4 | screen_name varchar(255), -- if null, then admin isn't impersonating
5 | created_at timestamp not null default current_timestamp,
6 | updated_at timestamp not null default current_timestamp
7 | );
8 |
9 | --- split here ---
10 |
11 | --------------------------------------------------------------------------------
12 | ---- auto-update updated_at ----------------------------------------------------
13 | --------------------------------------------------------------------------------
14 |
15 | CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at()
16 | RETURNS trigger
17 | LANGUAGE plpgsql
18 | AS $function$
19 | DECLARE
20 | _new record;
21 | BEGIN
22 | _new := NEW;
23 | _new."updated_at" = NOW();
24 | RETURN _new;
25 | END;
26 | $function$;
27 |
28 | --- split here ---
29 |
30 | CREATE TRIGGER set_updated_at
31 | BEFORE UPDATE ON impersonation
32 | FOR EACH ROW
33 | EXECUTE FUNCTION set_current_timestamp_updated_at();
34 |
--------------------------------------------------------------------------------
/src/ketchup/auth.clj:
--------------------------------------------------------------------------------
1 | (ns ketchup.auth
2 | (:require [buddy.sign.jwt :as jwt]
3 | [clojure.string :as str]
4 | [cheshire.core :as json]
5 | [ketchup.env :as env]))
6 |
7 | (defn create-auth-token [user-id]
8 | {:pre [(some? user-id)]}
9 | (jwt/sign {:user-id user-id} (env/get-env-var "JWT_SECRET_KEY")))
10 |
11 | (defn verify-auth-token [auth-token]
12 | (try
13 | (jwt/unsign auth-token (env/get-env-var "JWT_SECRET_KEY"))
14 | (catch Exception _e
15 | nil)))
16 |
17 | (defn get-authorization-token [req]
18 | (some-> (get-in req [:headers "authorization"])
19 | (str/split #"\s+")
20 | (second)))
21 |
22 | (def unauthorized-response
23 | {:status 401
24 | :headers {"Content-Type" "application/json"}
25 | :body (json/generate-string {:error "unauthorized to ketchup API"})})
26 |
27 | (defn wrap-authenticated [handler]
28 | (fn [request]
29 | (if-let [auth-token (get-authorization-token request)]
30 | (if-let [jwt (verify-auth-token auth-token)]
31 | (handler (assoc request :auth/parsed-jwt jwt))
32 | unauthorized-response)
33 | unauthorized-response)))
--------------------------------------------------------------------------------
/resources/public/heroku-errors/application-error.html:
--------------------------------------------------------------------------------
1 |
7 |
32 |
7 |
Privacy Policy
8 |
Small World operates the https://small-world-friends.herokuapp.com website, which provides the Service.
9 |
10 | This page is used to inform website visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service, the Small World website.
11 |
12 |
13 | If you choose to use our Service, then you agree to the collection and use of information in relation with this policy. The Personal Information that we collect are used for providing and
14 | improving the Service. We will not use or share your information with anyone except as described in this Privacy Policy.
15 |
16 |
17 |
18 | For a better experience while using our Service, we may require you to provide us with certain personally identifiable information, including but not limited to information from your Twitter
19 | profile. The information that we collect will be used to contact or identify you.
20 |
21 |
Log Data
22 |
23 | We want to inform you that whenever you visit our Service, we collect information that your browser sends to us that is called Log Data. This Log Data may include information such as your
24 | computer’s Internet Protocol ("IP") address, browser version, pages of our Service that you visit, the time and date of your visit, the time spent on those pages, and other statistics.
25 |
26 |
Cookies
27 |
28 | Cookies are files with small amount of data that is commonly used an anonymous unique identifier. These are sent to your browser from the website that you visit and are stored on your computer’s
29 | hard drive.
30 |
31 |
32 | Our website uses these "cookies" to collection information and to improve our Service. You have the option to either accept or refuse these cookies, and know when a cookie is being sent
33 | to your computer. If you choose to refuse our cookies, you may not be able to use some portions of our Service.
34 |
35 |
Service Providers
36 |
We may employ third-party companies and individuals due to the following reasons:
37 |
38 | - To facilitate our Service;
39 | - To provide the Service on our behalf;
40 | - To perform Service-related services; or
41 | - To assist us in analyzing how our Service is used.
42 |
43 |
44 | We want to inform our Service users that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are
45 | obligated not to disclose or use the information for any other purpose.
46 |
47 |
Security
48 |
49 | We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the
50 | internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security.
51 |
52 |
Links to Other Sites
53 |
54 | Our Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly
55 | advise you to review the Privacy Policy of these websites. We have no control over, and assume no responsibility for the content, privacy policies, or practices of any third-party sites or
56 | services.
57 |
58 |
Children's Privacy
59 |
60 | Our Services do not address anyone under the age of 13. We do not knowingly collect personal identifiable information from children under 13. In the case we discover that a child under 13 has
61 | provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information,
62 | please contact us so that we will be able to do necessary actions.
63 |
64 |
Changes to This Privacy Policy
65 |
66 | We may update our Privacy Policy from time to time. Thus, we advise you to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on
67 | this page. These changes are effective immediately, after they are posted on this page.
68 |
69 |
70 |
71 | If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us: https://github.com/devonzuegel/smallworld
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/smallworld/coordinates.clj:
--------------------------------------------------------------------------------
1 | (ns smallworld.coordinates (:require [clojure.data.json :as json]
2 | [smallworld.util :as util]
3 | [smallworld.memoize :as m]
4 | [smallworld.db :as db]))
5 |
6 | (def debug? false)
7 |
8 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
9 | ;; coordinate data fetching ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
10 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
11 |
12 | ;; if you need to make the locations more precise in the future, use the bbox
13 | ;; (bounding box) rather than just the coordinates
14 | (defn extract-coordinates [raw-result]
15 | (let [result (json/read-str raw-result)
16 | status-code (get result "statusCode")
17 | coordinates (get-in result ["resourceSets" 0 "resources" 0 "geocodePoints" 0 "coordinates"])
18 | coordinates {:lat (first coordinates) :lng (second coordinates)}]
19 | (if (= 200 status-code) coordinates
20 | (print "houston, we have a problem..."))))
21 |
22 | (defn get-from-city [city-str]
23 | (if (or (empty? city-str) (nil? city-str))
24 | (do (when debug? (println "city-str:" city-str))
25 | nil ; return nil coordinates if no city string is given TODO: return :no-result
26 | )
27 | (try
28 | (let [city (java.net.URLEncoder/encode (or city-str "") "UTF-8") ;; the (if empty?) shouldn't caught the nil string thing... not sure why it didn't
29 | api-key (java.net.URLEncoder/encode (util/get-env-var "BING_MAPS_API_KEY") "UTF-8")]
30 | (when debug?
31 | (println "city:" city)
32 | (println "api-key:" api-key)
33 | (println ""))
34 | (-> (str "https://dev.virtualearth.net/REST/v1/Locations/" city "?key=" api-key)
35 | slurp
36 | extract-coordinates))
37 | (catch Throwable e
38 | (println "\nBing Maps API - returning nil, because API call failed to retrieve a valid result")
39 | nil))))
40 |
41 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
42 | ;; calculations ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
43 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
44 |
45 | (defn haversine [{lon1 :lng lat1 :lat} {lon2 :lng lat2 :lat}]
46 | ; Haversine formula
47 | ; a = sin²(Δφ/2) + cos φ1 ⋅ cos φ2 ⋅ sin²(Δλ/2)
48 | ; c = 2 ⋅ atan2( √a, √(1−a) )
49 | ; d = R ⋅ c
50 | ; where φ is latitude, λ is longitude, R is earth’s radius (mean radius = 6,371km);
51 | ; "Implementation of Haversine formula. Takes two sets of latitude/longitude pairs and returns the shortest great circle distance between them (in km)"
52 | (assert (every? some? [lon1 lat1 lon2 lat2]) "coordinates {:lat, :lng} must not be nil")
53 | (let [R 6378.137 ; radius of Earth in km
54 | dlat (Math/toRadians (- lat2 lat1))
55 | dlon (Math/toRadians (- lon2 lon1))
56 | lat1 (Math/toRadians lat1)
57 | lat2 (Math/toRadians lat2)
58 | a (+ (* (Math/sin (/ dlat 2)) (Math/sin (/ dlat 2)))
59 | (* (Math/sin (/ dlon 2)) (Math/sin (/ dlon 2)) (Math/cos lat1) (Math/cos lat2)))]
60 | (* R 2 (Math/asin (Math/sqrt a)))))
61 |
62 | (defn coordinates-not-defined? [coords]
63 | (or (nil? coords)
64 | (some nil? (vals coords))))
65 |
66 | (defn distance-btwn [coords1 coords2]
67 | (when debug?
68 | (println "coords1:" coords1)
69 | (println "coords2:" coords2)
70 | (println ""))
71 | (if (or (coordinates-not-defined? coords1)
72 | (coordinates-not-defined? coords2))
73 | nil
74 | (haversine coords1 coords2)))
75 |
76 | (defn tokenize [s] (set (re-seq #"\w+" (clojure.string/lower-case s))))
77 |
78 | (defn jaccard [s1 s2]
79 | (/ (count (clojure.set/intersection (tokenize s1) (tokenize s2)))
80 | (count (clojure.set/union (tokenize s1) (tokenize s2)))))
81 |
82 | (defn very-similar-location-names [pairs]
83 | ; Don't consider them very similar if there is a slash or ampersand in either
84 | ; of the names, because that means there might be two locations in the string.
85 | (let [result (if (or (re-find #"/" (first pairs))
86 | (re-find #"/" (second pairs))
87 | (re-find #"&" (first pairs))
88 | (re-find #"&" (second pairs)))
89 | false
90 | (> (apply jaccard pairs) 0.5))]
91 | (println " pairs:" pairs)
92 | (println "very-similar-location-names?" result)
93 | (println " jaccard:" (apply jaccard pairs))
94 | (println "")
95 | result))
96 |
97 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
98 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
99 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
100 |
101 |
102 | (def table-memoized (m/my-memoize get-from-city db/coordinates-table))
103 | (def atom-memoized (m/my-memoize table-memoized (atom {})))
104 | (def memoized atom-memoized) ; re-naming just for export
105 |
--------------------------------------------------------------------------------
/src/meetcute/util.cljc:
--------------------------------------------------------------------------------
1 | (ns meetcute.util
2 | (:require [clojure.string :as str]
3 | [clojure.walk :refer [keywordize-keys]]))
4 |
5 | (defn remove-nth [lst n]
6 | (println "remove-nth: " n)
7 | (concat (take n lst) (drop (inc n) lst)))
8 |
9 | (defn clean-email
10 | [email]
11 | (str/lower-case (str/trim email)))
12 |
13 | (defn valid-email? [email]
14 | (and (string? email)
15 | (not (empty? email))))
16 |
17 | (defn valid-code? [code]
18 | (and (string? code)
19 | (re-matches #"^\d{6}$" code)))
20 |
21 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
22 | ;; phone utils
23 |
24 | (defn clean-phone
25 | "The standard format is +{AREA}{NUMBER} without separators. Examples:
26 |
27 | +14159499931
28 | +16507919090
29 | +5491137560419"
30 | [phone]
31 | (let [only-numbers (some-> phone (str/replace #"[^0-9]" ""))]
32 | (some->> only-numbers (str "+"))))
33 |
34 | ;; TODO: use a proper validation function
35 | (defn valid-phone?
36 | "Strips the phone number of all non-numeric characters, then check if it's a valid phone number. "
37 | [phone]
38 | (let [phone (or (some-> phone clean-phone) "")]
39 | (and (not-empty phone)
40 | (<= 9 (count phone)))))
41 |
42 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
43 | ;; profile utils
44 |
45 | (defn get-field [bio field]
46 | (get-in bio [(keyword field)]))
47 |
48 | (defn get-gender-filter [bio]
49 | (case (get-field bio "I'm interested in...")
50 | [] []
51 | ["Women"] ["Woman"]
52 | ["Men"] ["Man"]
53 | ["Woman" "Man"] ; default
54 | ))
55 |
56 | (defn included-bios [profile cuties]
57 | (let [profile (keywordize-keys profile)
58 | cuties (keywordize-keys cuties)
59 | matches-preferences? (fn [cutie]
60 | ;; (println)
61 | ;; (println (get-field profile "First name") " gets to meet " (get-field cutie "First name"))
62 | ;; (println "Include in gallery? " (get-field cutie "Include in gallery?"))
63 | ;; (println " " (= (get-field cutie "Include in gallery?") "include in gallery"))
64 |
65 | ;; (println "passes the filter? " (and (= (get-field cutie "Include in gallery?") "include in gallery") ; don't include bios that have been explicitly excluded
66 | ;; (not (= (:id cutie) (:id profile))) ; check the cutie is not themself
67 | ;; (some #(= (:Gender cutie) %) (get-gender-filter profile)) ; only show the gender that the user is interested in dating
68 | ;; (some #(= (:Gender profile) %) (get-gender-filter cutie)) ; only show someone if they're interested in dating someone of the gender of the current user:
69 | ;; ))
70 |
71 | (and (= (get-field cutie "Include in gallery?") "include in gallery") ; don't include bios that have been explicitly excluded
72 | (not (= (:id cutie) (:id profile))) ; check the cutie is not themself
73 | (some #(= (:Gender cutie) %) (get-gender-filter profile)) ; only show the gender that the user is interested in dating
74 | (some #(= (:Gender profile) %) (get-gender-filter cutie)) ; only show someone if they're interested in dating someone of the gender of the current user:
75 | ))]
76 | (filter matches-preferences? cuties)))
77 |
78 | (def fields-changeable-by-user [; Phone is intentionally not included because it's used as the key to find the record to update, so we don't want to overwrite it
79 | "Anything else you'd like your potential matches to know?"
80 | "Social media links"
81 | "Email"
82 | "First name"
83 | "Last name"
84 | "Phone"
85 | "include-in-gallery?"
86 | "Home base city"
87 | "Other cities where you spend time"
88 | "locations-json"
89 | "I'm interested in..."
90 | "If 'Other', who invited you?"
91 | "Birthday"
92 | "unseen-cuties"
93 | "todays-cutie"
94 | "selected-cuties"
95 | "rejected-cuties"
96 | "What makes this person awesome?"
97 | "Pictures"
98 | "Gender"])
99 |
100 | (defn in? [list str] (some #(= str %) list))
101 |
--------------------------------------------------------------------------------
/src/meetcute/routes.clj:
--------------------------------------------------------------------------------
1 | (ns meetcute.routes
2 | (:require [compojure.core :as compo :refer [defroutes GET POST ANY]]
3 | [compojure.route :as route]
4 | [clojure.java.io :as io]
5 | [clojure.string :as str]
6 | [cheshire.core :as json]
7 | [meetcute.logic :as logic]
8 | [smallworld.util :as sw-util]
9 | [meetcute.auth :as mc.auth]
10 | [meetcute.util :as mc.util]
11 | [ring.util.mime-type :as mime]
12 | [ring.util.request]
13 | [ring.util.response :as resp]))
14 |
15 | (defn parse-body-params [body]
16 | (json/parse-string body true))
17 |
18 | (defn- add-wildcard [^String path]
19 | (str path (if (.endsWith path "/") "*" "/*")))
20 |
21 | (defn- add-mime-type [response path options]
22 | (if-let [mime-type (mime/ext-mime-type path (:mime-types options {}))]
23 | (resp/content-type response mime-type)
24 | response))
25 |
26 | (defn resources
27 | ([path]
28 | (resources path {}))
29 | ([path options]
30 | (GET (add-wildcard path) {{resource-path :*} :route-params}
31 | (let [resource-path (str/replace resource-path (re-pattern "^/meetcute/") ; only the first occurrence, anchored to the beginning of the string
32 | "")
33 | root "public"]
34 | (some-> (resp/resource-response (str root "/" resource-path))
35 | (add-mime-type resource-path options))))))
36 |
37 | #_(defn tmp-upload-handler [request]
38 | (if-let [file (-> request :params :file)]
39 | (let [phone (some-> (mc.auth/req->parsed-jwt request) :auth/phone mc.util/clean-phone)
40 | cutie (logic/my-profile phone :force-refresh? true)]
41 | (println "file: " file)
42 | (println "filename: " (:filename file))
43 | (println "cutie id: " (:id cutie))
44 | (println " phone: " phone)
45 | (io/copy (:tempfile file)
46 | (io/file (str "resources/public/tmp-img-uploads/" (:filename file))))
47 | (logic/update-cutie-picture (:id cutie)
48 | (str "https://7138-186-177-83-218.ngrok-free.app/tmp-img-uploads/" (:filename file)))
49 | (resp/redirect "/meetcute/settings"))
50 | (resp/response "No file provided")))
51 |
52 | (defn tmp-file-path [file]
53 | (if (= (:prod sw-util/ENVIRONMENTS) (sw-util/get-env-var "ENVIRONMENT"))
54 | (str "https://smallworld.kiwi/tmp/" (:filename file))
55 | (do
56 | (println "
7 |
Terms and Conditions
8 |
Last updated: February 17, 2022
9 |
Please read these terms and conditions carefully before using Our Service.
10 |
Interpretation and Definitions
11 |
Interpretation
12 |
13 | The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear
14 | in singular or in plural.
15 |
16 |
Definitions
17 |
For the purposes of these Terms and Conditions:
18 |
19 | -
20 |
21 | Affiliate means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares,
22 | equity interest or other securities entitled to vote for election of directors or other managing authority.
23 |
24 |
25 | -
26 |
Country refers to: Florida, United States
27 |
28 | -
29 |
Company (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Small World.
30 |
31 | -
32 |
Device means any device that can access the Service such as a computer, a cellphone or a digital tablet.
33 |
34 | -
35 |
Service refers to the Website.
36 |
37 | -
38 |
39 | Terms and Conditions (also referred as "Terms") mean these Terms and Conditions that form the entire agreement between You and the Company regarding the use of the
40 | Service. This Terms and Conditions agreement has been created with the help of the
41 | Terms and Conditions Generator.
42 |
43 |
44 | -
45 |
46 | Third-party Social Media Service means any services or content (including data, information, products or services) provided by a third-party that may be displayed, included or
47 | made available by the Service.
48 |
49 |
50 | -
51 |
52 | Website refers to Small World, accessible from
53 | https://small-world-friends.herokuapp.com
54 |
55 |
56 | -
57 |
58 | You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as
59 | applicable.
60 |
61 |
62 |
63 |
Acknowledgment
64 |
65 | These are the Terms and Conditions governing the use of this Service and the agreement that operates between You and the Company. These Terms and Conditions set out the rights and obligations of
66 | all users regarding the use of the Service.
67 |
68 |
69 | Your access to and use of the Service is conditioned on Your acceptance of and compliance with these Terms and Conditions. These Terms and Conditions apply to all visitors, users and others who
70 | access or use the Service.
71 |
72 |
By accessing or using the Service You agree to be bound by these Terms and Conditions. If You disagree with any part of these Terms and Conditions then You may not access the Service.
73 |
You represent that you are over the age of 18. The Company does not permit those under 18 to use the Service.
74 |
75 | Your access to and use of the Service is also conditioned on Your acceptance of and compliance with the Privacy Policy of the Company. Our Privacy Policy describes Our policies and procedures on
76 | the collection, use and disclosure of Your personal information when You use the Application or the Website and tells You about Your privacy rights and how the law protects You. Please read Our
77 | Privacy Policy carefully before using Our Service.
78 |
79 |
Links to Other Websites
80 |
Our Service may contain links to third-party web sites or services that are not owned or controlled by the Company.
81 |
82 | The Company has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that the
83 | Company shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with the use of or reliance on any such content, goods
84 | or services available on or through any such web sites or services.
85 |
86 |
We strongly advise You to read the terms and conditions and privacy policies of any third-party web sites or services that You visit.
87 |
Termination
88 |
We may terminate or suspend Your access immediately, without prior notice or liability, for any reason whatsoever, including without limitation if You breach these Terms and Conditions.
89 |
Upon termination, Your right to use the Service will cease immediately.
90 |
Limitation of Liability
91 |
92 | Notwithstanding any damages that You might incur, the entire liability of the Company and any of its suppliers under any provision of this Terms and Your exclusive remedy for all of the foregoing
93 | shall be limited to the amount actually paid by You through the Service or 100 USD if You haven't purchased anything through the Service.
94 |
95 |
96 | To the maximum extent permitted by applicable law, in no event shall the Company or its suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever (including,
97 | but not limited to, damages for loss of profits, loss of data or other information, for business interruption, for personal injury, loss of privacy arising out of or in any way related to the use
98 | of or inability to use the Service, third-party software and/or third-party hardware used with the Service, or otherwise in connection with any provision of this Terms), even if the Company or any
99 | supplier has been advised of the possibility of such damages and even if the remedy fails of its essential purpose.
100 |
101 |
102 | Some states do not allow the exclusion of implied warranties or limitation of liability for incidental or consequential damages, which means that some of the above limitations may not apply. In
103 | these states, each party's liability will be limited to the greatest extent permitted by law.
104 |
105 |
"AS IS" and "AS AVAILABLE" Disclaimer
106 |
107 | The Service is provided to You "AS IS" and "AS AVAILABLE" and with all faults and defects without warranty of any kind. To the maximum extent permitted under applicable law,
108 | the Company, on its own behalf and on behalf of its Affiliates and its and their respective licensors and service providers, expressly disclaims all warranties, whether express, implied, statutory
109 | or otherwise, with respect to the Service, including all implied warranties of merchantability, fitness for a particular purpose, title and non-infringement, and warranties that may arise out of
110 | course of dealing, course of performance, usage or trade practice. Without limitation to the foregoing, the Company provides no warranty or undertaking, and makes no representation of any kind
111 | that the Service will meet Your requirements, achieve any intended results, be compatible or work with any other software, applications, systems or services, operate without interruption, meet any
112 | performance or reliability standards or be error free or that any errors or defects can or will be corrected.
113 |
114 |
115 | Without limiting the foregoing, neither the Company nor any of the company's provider makes any representation or warranty of any kind, express or implied: (i) as to the operation or availability
116 | of the Service, or the information, content, and materials or products included thereon; (ii) that the Service will be uninterrupted or error-free; (iii) as to the accuracy, reliability, or
117 | currency of any information or content provided through the Service; or (iv) that the Service, its servers, the content, or e-mails sent from or on behalf of the Company are free of viruses,
118 | scripts, trojan horses, worms, malware, timebombs or other harmful components.
119 |
120 |
121 | Some jurisdictions do not allow the exclusion of certain types of warranties or limitations on applicable statutory rights of a consumer, so some or all of the above exclusions and limitations may
122 | not apply to You. But in such a case the exclusions and limitations set forth in this section shall be applied to the greatest extent enforceable under applicable law.
123 |
124 |
Governing Law
125 |
126 | The laws of the Country, excluding its conflicts of law rules, shall govern this Terms and Your use of the Service. Your use of the Application may also be subject to other local, state, national,
127 | or international laws.
128 |
129 |
Disputes Resolution
130 |
If You have any concern or dispute about the Service, You agree to first try to resolve the dispute informally by contacting the Company.
131 |
For European Union (EU) Users
132 |
If You are a European Union consumer, you will benefit from any mandatory provisions of the law of the country in which you are resident in.
133 |
United States Legal Compliance
134 |
135 | You represent and warrant that (i) You are not located in a country that is subject to the United States government embargo, or that has been designated by the United States government as a
136 | "terrorist supporting" country, and (ii) You are not listed on any United States government list of prohibited or restricted parties.
137 |
138 |
Severability and Waiver
139 |
Severability
140 |
141 | If any provision of these Terms is held to be unenforceable or invalid, such provision will be changed and interpreted to accomplish the objectives of such provision to the greatest extent
142 | possible under applicable law and the remaining provisions will continue in full force and effect.
143 |
144 |
Waiver
145 |
146 | Except as provided herein, the failure to exercise a right or to require performance of an obligation under these Terms shall not effect a party's ability to exercise such right or require such
147 | performance at any time thereafter nor shall the waiver of a breach constitute a waiver of any subsequent breach.
148 |
149 |
Translation Interpretation
150 |
These Terms and Conditions may have been translated if We have made them available to You on our Service. You agree that the original English text shall prevail in the case of a dispute.
151 |
Changes to These Terms and Conditions
152 |
153 | We reserve the right, at Our sole discretion, to modify or replace these Terms at any time. If a revision is material We will make reasonable efforts to provide at least 30 days' notice prior to
154 | any new terms taking effect. What constitutes a material change will be determined at Our sole discretion.
155 |
156 |
157 | By continuing to access or use Our Service after those revisions become effective, You agree to be bound by the revised terms. If You do not agree to the new terms, in whole or in part, please
158 | stop using the website and the Service.
159 |
160 |
Contact Us
161 |
If you have any questions about these Terms and Conditions, You can contact us:
162 |
165 |
166 |
--------------------------------------------------------------------------------
/src/smallworld/screens/home.cljs:
--------------------------------------------------------------------------------
1 | (ns smallworld.screens.home
2 | (:require [clojure.string :as str]
3 | [reagent.core :as r]
4 | [reitit.frontend.easy :as rfe]
5 | [smallworld.decorations :as decorations]
6 | [smallworld.mapbox :as mapbox]
7 | [smallworld.session :as session]
8 | [smallworld.user-data :as user-data]
9 | [smallworld.util :as util]
10 | [smallworld.screens.settings :as settings]))
11 |
12 | (def *debug? (r/atom false))
13 | (defonce *minimaps (r/atom {}))
14 |
15 | (defn minimap [minimap-id location-name coords]
16 | (r/create-class {:component-did-mount
17 | (fn [] ; this should be called just once when the component is mounted
18 | (swap! *minimaps assoc minimap-id
19 | (new js/mapboxgl.Map
20 | #js{:container minimap-id
21 | :key (get-in mapbox/config [mapbox/style :access-token])
22 | :style (get-in mapbox/config [mapbox/style :style])
23 | :center (clj->js [(:lng coords) (:lat coords)])
24 | :interactive false ; makes the map not zoomable or draggable
25 | :attributionControl false ; removes the Mapbox copyright symbol
26 | :zoom 1
27 | :maxZoom 8
28 | :minZoom 0}))
29 | ; zoom out if they haven't provided a location
30 | (when (clojure.string/blank? location-name)
31 | (.setZoom (get @*minimaps minimap-id) 0)))
32 | :reagent-render (fn [] [:div {:id minimap-id}])}))
33 |
34 | (defn debugger-btn []
35 | (when (= (.. js/window -location -hash) "#debug")
36 | [:p {:style {:text-align "center"}}
37 | [:a {:on-click #(reset! *debug? (not @*debug?)) :href "#" :style {:border-bottom "2px solid #ffffff33"}}
38 | "toggle debug – currently " (if @*debug? "on 🟢" "off 🔴")]]))
39 |
40 | (defn debugger-info []
41 | (when @*debug?
42 | [:<> [:br] [:br] [:hr]
43 | [:div.refresh-friends {:style {:margin-top "64px" :text-align "center"}}
44 | [:a.btn {:href "#"
45 | :on-click #(util/fetch "/api/v1/friends"
46 | (fn [result]
47 | (reset! user-data/*friends result)
48 | (println settings/*settings)
49 | (js/setTimeout (mapbox/add-friends-to-map @user-data/*friends @settings/*settings) 2000))
50 | :retry? true)}
51 | "pull friends again (without refreshing from Twitter!)"]]
52 |
53 | [:br]
54 | [:div.refresh-friends {:style {:margin-top "64px" :text-align "center"}}
55 | [:div {:style {:margin-bottom "12px" :font-size "0.9em"}}
56 | "does the data for your friends look out of date?"]
57 | [:a.btn {:href "#" :on-click user-data/refetch-friends}
58 | "refresh friends"]
59 | [:div {:style {:margin-top "12px" :font-size "0.8em" :opacity "0.6" :font-family "Inria Serif, serif" :font-style "italic"}}
60 | "note: this takes several seconds to run"]]
61 |
62 | [:br]
63 | [:b "current-user:"]
64 | [:pre (util/preify @session/*store)] [:br]
65 | [:b "settings:"]
66 | [:pre (util/preify @settings/*settings)] [:br]
67 | [:b "@user-data/*friends: (showing max of 5 only)"]
68 | (if (= @user-data/*friends :loading)
69 | [:pre "@user-data/*friends is still :loading"]
70 | [:pre "count:" (count @user-data/*friends) "\n\n" (util/preify (take 5 @user-data/*friends))])]))
71 |
72 | ; TODO: lots of low-hanging fruit to improve performance
73 | (defn fetch-coordinates! [minimap-id location-name-input i]
74 | (if (str/blank? location-name-input) ; don't fetch from the API if the input is blank
75 | (.flyTo (get @*minimaps minimap-id) #js{:essential true ; this animation is essential with respect to prefers-reduced-motion
76 | :zoom 0})
77 | (util/fetch-post "/api/v1/coordinates" {:location-name location-name-input}
78 | (fn [result]
79 | (.flyTo (get @*minimaps minimap-id)
80 | #js{:essential true ; this animation is essential with respect to prefers-reduced-motion
81 | :zoom 1
82 | :center #js[(:lng result) (:lat result)]})
83 | (when (and (:lat result) (:lng result))
84 | (swap! settings/*settings assoc-in [:locations i :coords] result)
85 | (user-data/recompute-friends
86 | #(swap! settings/*settings assoc-in [:locations i :loading] false))
87 | (util/fetch-post "/api/v1/settings/update" ; persist the changes to the server
88 | {:locations (:locations (assoc-in @settings/*settings [:locations i :loading] false))}))))))
89 |
90 | (def fetch-coordinates-debounced! (util/debounce fetch-coordinates! 400))
91 |
92 | (defn from-twitter? [location-data]
93 | (or (= (:special-status location-data) "twitter-location")
94 | (= (:special-status location-data) "from-display-name")))
95 |
96 | (defn -screen []
97 | [:<>
98 | (util/nav)
99 | (let [curr-user-locations (remove nil? (:locations @settings/*settings))
100 | update [:a {:href "https://twitter.com/settings/profile" :target "_blank"} "update"]
101 | track-new-location-btn [:div#track-new-location-field
102 | {:on-click (fn []
103 | (let [updated-locations (vec (concat curr-user-locations ; using concat instead of conj so it adds to the end
104 | [{:special-status "added-manually"
105 | :name "" ; the value starts out blank
106 | :coords nil}]))]
107 | (swap! settings/*settings assoc :locations updated-locations)
108 |
109 | (js/setTimeout #(do
110 | (-> (last (util/query-dom ".friends-list input"))
111 | .focus)
112 | (-> (last (util/query-dom ".category"))
113 | (.scrollIntoView #js{:behavior "smooth" :block "center" :inline "center"})))
114 | 50)))}
115 | (decorations/plus-icon "scale(0.15)") "follow a new location"]]
116 | [:div.home-page
117 | [:div.announcement-banner
118 | [:p.header "JULY 25, 2023:"]
119 | [:p "Sadly, Small World's Twitter API access has been " [:a {:href "https://twitter.com/devonzuegel/status/1681137630019481601" :target "_blank"} "shut off"] ", so your data won't refresh anymore. "]
120 | [:p "The good news is that I've found an alternative: Fedica, which has " [:a {:href "https://fedica.com/twitter/map-people-you-follow?_by=smallworld&fp_sid=kiwi" :target "_blank"} "a similar map feature as Small World"] ". Enjoy!"]
121 | [:p "Small World's stale data might still be useful, since people don't move that often. And if I find a way to get API access in the future, I'll let you know."]
122 | [:p "— Devon"]]
123 | (let [top-location (first (remove nil? (:locations @settings/*settings)))]
124 | [util/error-boundary
125 | [mapbox/mapbox
126 | {:lng-lat (:coords top-location)
127 | :location (:name top-location)
128 | :user-img (:profile_image_url_large @session/*store)
129 | :user-name (:name @session/*store)
130 | :screen-name (:screen-name @session/*store)}]])
131 |
132 | (when (not= 0 (count curr-user-locations))
133 | track-new-location-btn)
134 |
135 | (doall (map-indexed
136 | (fn [i location-data]
137 | (let [minimap-id (str "minimap-location-" i)]
138 | [:div.category {:key i}
139 | [:div.friends-list.header
140 |
141 | [:div.left-side.mapbox-container
142 | [minimap minimap-id (:name location-data) (:coords location-data)]
143 | (when-not (str/blank? (:name location-data)) [:div.center-point])]
144 |
145 | [:div.right-side
146 | [:div.based-on (condp = (:special-status location-data)
147 | "twitter-location" "based on your Twitter location, you live in:"
148 | "from-display-name" "based on your Twitter name, you're visiting:"
149 | nil)]
150 | [:input {:type "text"
151 | :value (:name location-data)
152 | :autoComplete "off"
153 | :auto-complete "off"
154 | :style {:cursor (when-not (from-twitter? location-data) "pointer")}
155 | :readOnly (from-twitter? location-data) ; don't allow them to edit the Twitter locations
156 | :placeholder "type a location to follow"
157 | :on-change #(let [input-elem (.-target %)
158 | new-value (.-value input-elem)]
159 | (swap! settings/*settings assoc-in [:locations i :loading] true)
160 | (fetch-coordinates-debounced! minimap-id new-value i)
161 | (swap! settings/*settings assoc-in [:locations i :name] new-value)
162 | (util/fetch-post "/api/v1/settings/update" {:locations (:locations @settings/*settings)}))}]
163 | #_(when (from-twitter? location-data) ; no longer needed because they aren't editable
164 | [:div.small-info-text "this won't update your Twitter profile :)"])]
165 |
166 | [:div.delete-location-btn {:title "delete this location"
167 | :on-click #(when (js/confirm "are you sure that you want to delete this location? don't worry, you can add it back later any time")
168 | (let [updated-locations (util/rm-from-list curr-user-locations i)]
169 | (println "settings/*settings :locations (BEFORE)")
170 | (println (:locations @settings/*settings))
171 | (swap! settings/*settings assoc :locations updated-locations)
172 | (println "settings/*settings :locations (AFTER)")
173 | (println (:locations @settings/*settings))
174 | (util/fetch-post "/api/v1/settings/update" {:locations updated-locations})))}
175 | (decorations/cancel-icon)]]
176 |
177 | (if (or (get-in @settings/*settings [:locations i :loading])
178 | (= [] @user-data/*friends)
179 | (= :loading @user-data/*friends))
180 | [:div.friends-list [:div.loading (decorations/simple-loading-animation) "fetching your Twitter friends..."]]
181 | (when-not (nil? (:coords location-data))
182 | [:<> ; TODO: refactor this so that data is passed in as a param, rather than depending on side effects
183 | (user-data/render-friends-list i "twitter-location" "based near" (:name location-data))
184 | (user-data/render-friends-list i "from-display-name" "visiting" (:name location-data))]))]))
185 | curr-user-locations))
186 |
187 | [:div.no-locations-info
188 | [:p "3 ways to start following a location:"]
189 | [:ul
190 | [:li update " your Twitter profile location"]
191 | [:li update " your Twitter display name (e.g. \"Devon in NYC\")"] ; TODO: make this an `i` info hover
192 | [:li "add a location manually:"]]
193 | track-new-location-btn]
194 |
195 | [:br] [:br]
196 | (debugger-btn)
197 |
198 | (settings/info-footer (:screen-name @session/*store)
199 | user-data/recompute-friends) ; TODO: replace with doseq, which is for side effects
200 | (debugger-info)])])
201 |
202 | (defn screen []
203 | (r/create-class
204 | {:component-did-mount
205 | (fn []
206 | (settings/refresh-friends) ; refresh immediately
207 | (doall (for [i (range 2 5)] ; then refresh it again, with exponential backoff
208 | (js/setTimeout settings/refresh-friends (* (util/exponent 2 i) 1000)))))
209 | :reagent-render (fn [] [-screen])}))
210 |
--------------------------------------------------------------------------------
/resources/public/css/meetcute.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | border: 0;
4 | outline: 0;
5 | font-size: 100%;
6 | vertical-align: baseline;
7 | background: transparent;
8 | }
9 |
10 | html {
11 | color: #2b2017;
12 | // color: #013917;
13 | background: #fcfcfc;
14 | // for a hacky dark mode when coding at night:
15 | // background: #2b2017;
16 | // color: #ccc;
17 | font-family: 'Roboto Mono', monospace;
18 | }
19 |
20 | pre {
21 | text-wrap: wrap;
22 | }
23 |
24 | a {
25 | color: rgb(46, 103, 63);
26 | font-weight: 700;
27 | text-decoration: none;
28 | // background: rgb(42, 98, 59, 0.1);
29 | // padding: 2px 4px;
30 | border-bottom: 2px solid transparent;
31 | border-bottom: 2px solid rgb(42, 98, 59, 0.2);
32 | transition: border-bottom 0.1s ease-in-out;
33 | }
34 |
35 | a:hover {
36 | // text-decoration: underline;
37 | border-bottom: 2px solid rgb(42, 98, 59, 0.3);
38 | transition: border-bottom 0.1s ease-in-out;
39 | }
40 |
41 | button,
42 | a,
43 | textarea {
44 | font-family: 'Roboto Mono', monospace;
45 | }
46 |
47 | .btn {
48 | color: rgb(188, 181, 175, 1);
49 | border: 3px solid rgb(188, 181, 175, 0);
50 | transition: border 0.2s ease-in-out;
51 | padding: 12px 14px;
52 | border-radius: 8px;
53 | cursor: pointer;
54 | margin: 6px;
55 | display: inline-block;
56 | text-decoration: none;
57 | font-weight: bold;
58 | }
59 |
60 | .btn:hover {
61 | color: rgb(108, 98, 90);
62 | transition: all 0.1s ease-in-out;
63 | border: 3px solid rgb(188, 181, 175, 0);
64 | }
65 |
66 | .btn.primary {
67 | border: 3px solid rgb(188, 181, 175, 0.4);
68 | }
69 |
70 | .btn.primary:hover {
71 | border: 3px solid rgb(188, 181, 175, 0.7);
72 | }
73 |
74 | .btn.primary.green {
75 | background: rgb(42, 98, 59, 0.08);
76 | border: 3px solid rgb(42, 98, 59, 0.8);
77 | color: rgb(42, 98, 59, 0.8);
78 | transition: all 0.1s ease-in-out;
79 | }
80 |
81 | .btn.primary.green:hover {
82 | background: rgb(42, 98, 59, 0.1);
83 | border: 3px solid rgb(42, 98, 59, 0.9);
84 | color: rgb(42, 98, 59, 1);
85 | transition: all 0.1s ease-in-out;
86 | }
87 |
88 | body {
89 | margin: 0;
90 | }
91 |
92 | html,
93 | body {
94 | height: 100%;
95 | }
96 |
97 | input {
98 | color: #220b01;
99 | transition: color 200ms ease-in-out;
100 | font-family: 'Roboto Mono', monospace;
101 | accent-color: #054921e0;
102 | }
103 |
104 | input:focus {
105 | color: #000000;
106 | transition: color 200ms ease-in-out;
107 | }
108 |
109 | input[type='radio'] {
110 | width: 20px;
111 | height: 20px;
112 | }
113 |
114 | input[type='checkbox'] {
115 | width: 16px;
116 | height: 16px;
117 | }
118 |
119 | .input-container input {
120 | border: none;
121 | box-sizing: border-box;
122 | outline: 0;
123 | padding: 0.75rem;
124 | position: relative !important;
125 | width: 100%;
126 | }
127 |
128 | input[type='date'] {
129 | cursor: pointer;
130 | }
131 |
132 | input[type='date']::-webkit-calendar-picker-indicator {
133 | width: 100%;
134 | height: 22px;
135 | cursor: pointer;
136 | // opacity: 0.1;
137 | // height: 20px;
138 | // position: absolute;
139 | // fill parent:
140 | // top: 0;
141 | // left: 0;
142 | // right: 0;
143 | // bottom: 0;
144 | // visibility: hidden;
145 | }
146 |
147 | .input-date-overlay {
148 | display: block;
149 | margin-bottom: -38px;
150 | margin-left: 36px;
151 | top: 0px;
152 | padding: 6px 0px;
153 | font-size: 16px;
154 | display: block;
155 | margin-bottom: -28px;
156 | margin-left: 36px;
157 | position: relative;
158 | pointer-events: none;
159 | }
160 |
161 | /* keep form inputs from zooming on mobile: https://www.warrenchandler.com/2019/04/02/stop-iphones-from-zooming-in-on-form-fields */
162 | body {
163 | line-height: 1.3em;
164 | font-size: 14px;
165 | }
166 |
167 | input,
168 | select {
169 | font-size: 16px;
170 | }
171 |
172 | ul {
173 | padding-inline-start: 20px;
174 | }
175 |
176 | li {
177 | padding-left: 4px;
178 | }
179 |
180 | .loading-container {
181 | position: fixed;
182 | top: 40%;
183 | left: 50%;
184 | transform: translate(-50%, -50%);
185 | }
186 |
187 | .loading-container .spinner {
188 | width: 120px;
189 | height: 120px;
190 | margin: auto;
191 | animation: spin 1.7s ease-in-out infinite;
192 | background-image: url('/orange-favicon.png');
193 | background-size: contain;
194 | }
195 |
196 | @keyframes spin {
197 | 0% {
198 | transform: rotate(0deg);
199 | }
200 |
201 | 100% {
202 | transform: rotate(360deg);
203 | }
204 | }
205 |
206 | .bio-row .title {
207 | width: fit-content;
208 | padding: 16px 12px 0 12px;
209 | opacity: 0.75;
210 | font-weight: bold;
211 | text-transform: uppercase;
212 | font-style: italic;
213 | color: #a19890;
214 | font-size: 0.8em;
215 | }
216 |
217 | .bio-row .required-note {
218 | color: rgb(255 106 0);
219 | font-size: 1.3em;
220 | text-transform: lowercase;
221 | margin: 0 4px;
222 | }
223 |
224 | .bio-row textarea {
225 | background: white;
226 | border: 3px solid rgba(188, 181, 175, 0.3);
227 | border-radius: 8px;
228 | resize: vertical;
229 | padding: 16px 20px;
230 | margin-right: 12px;
231 | min-height: 140px;
232 | color: #333;
233 | font-size: 0.9em;
234 | width: 98%;
235 | transition: all 0.2s ease-in-out;
236 | }
237 |
238 | .required textarea:empty:not(:focus),
239 | .required .editable-input.required-but-empty,
240 | .required .editable-input[value='']:not([type='date']):not(:focus) {
241 | background-color: rgb(255, 248, 243);
242 | border: 3px solid rgb(255, 181, 128);
243 | transition: all 0.2s ease-in-out;
244 | }
245 |
246 | .required .editable-input.required-but-empty input {
247 | background-color: rgb(255, 248, 243);
248 | }
249 |
250 | .bio-row-locations .bio-row {
251 | width: 100%;
252 | }
253 |
254 | .bio-row-value {
255 | max-width: 78vw;
256 | display: flex;
257 | }
258 |
259 | .bio-row-value input:focus:not([type='date']) {
260 | border: 3px solid rgba(188, 181, 175, 0.6) !important;
261 | transition: all 0.2s ease-in-out;
262 | }
263 |
264 | @media screen and (min-width: 600px) {
265 | .bio-row-value {
266 | min-width: 370px;
267 | }
268 | }
269 |
270 | .required-but-empty,
271 | .required-not-empty {
272 | padding: 6px 8px 8px 8px;
273 | border: 3px solid transparent;
274 | }
275 |
276 | .required-but-empty {
277 | background-color: rgb(255, 248, 243);
278 | border: 3px solid rgb(255, 181, 128);
279 | border-radius: 8px;
280 | }
281 |
282 | .editable-input {
283 | background: white;
284 | border: 3px solid rgba(188, 181, 175, 0.3);
285 | border-radius: 8px;
286 | padding: 6px 8px;
287 | margin-right: 4px;
288 | width: 95%;
289 | max-width: 380px;
290 | }
291 |
292 | .editable-input .editable-input {
293 | margin: 0;
294 | padding: 0;
295 | width: 100%;
296 | border: none;
297 | }
298 |
299 | input#display-phone,
300 | .iti--separate-dial-code .iti__selected-flag {
301 | background: rgba(35, 81, 49, 0.08) !important;
302 | }
303 |
304 | #display-phone::placeholder {
305 | color: rgba(25, 56, 34, 0.4);
306 | }
307 |
308 | input#email,
309 | .iti--separate-dial-code .iti__selected-flag {
310 | background: rgba(35, 81, 49, 0.08) !important;
311 | }
312 |
313 | #email::placeholder {
314 | color: rgba(25, 56, 34, 0.4);
315 | }
316 |
317 | .iti__country-list {
318 | max-width: 330px;
319 | overflow-x: hidden;
320 | font-size: 0.8em;
321 | }
322 |
323 | .oranges-wallpaper {
324 | background-color: #ffe2c0;
325 | background-image: url('../oranges-tile-repeat.jpg');
326 | background-size: 420px;
327 | background-image: url('../oranges-tile-repeat-2.jpg');
328 | background-size: min(700px, 90vw);
329 | background-position: 50% 0;
330 | background-position: 50% 0;
331 | height: calc(100vh - 15vh);
332 |
333 | // make children centered vertically on page, but no more than 120px from the top
334 | display: flex;
335 | flex-direction: column;
336 | justify-content: center;
337 | align-items: center;
338 | padding-top: 15vh;
339 | }
340 |
341 | .oranges-wallpaper form {
342 | padding-top: 48px;
343 | }
344 |
345 | .signin-form-background {
346 | box-shadow: 0 0 2px 0 rgba(255, 255, 255, 1);
347 | border: 4px solid rgb(42, 98, 59, 1);
348 | color: rgb(7, 69, 25);
349 | background: rgb(252 252 252 / 100%);
350 | max-width: 290px;
351 | margin-left: auto;
352 | margin-right: auto;
353 | width: 90%;
354 | padding: 24px;
355 | text-align: center;
356 | border-radius: 12px;
357 | }
358 |
359 | //////////////////////////////////////////////////
360 | // Expandable component
361 |
362 | // this currently doesn't work, not sure why... maybe because on plane?
363 | details {
364 | font-size: 1.4em;
365 | color: red;
366 | cursor: pointer;
367 | }
368 |
369 | details span.title {
370 | margin-left: 8px;
371 | font-size: 1.5em;
372 | }
373 |
374 | details>summary {
375 | // width: 100%;
376 | width: fit-content;
377 | // border-top: 1px solid #e0e0e0;
378 | padding: 32px 6px 6px 6px;
379 | cursor: pointer;
380 | font-weight: bold;
381 | }
382 |
383 | details>summary:first-of-type {
384 | border-top: none;
385 | margin-top: 8px;
386 | }
387 |
388 | details[open]>summary::marker,
389 | details>summary::marker,
390 | details summary::-webkit-details-marker,
391 | details summary::marker {
392 | font-size: 1.4em;
393 | display: inline-block;
394 | }
395 |
396 | //////////////////////////////////////////////////
397 | /// table
398 |
399 | table {
400 | border-collapse: collapse;
401 | border-spacing: 0;
402 | width: 100%;
403 | border: 1px solid #ddd;
404 | }
405 |
406 | th,
407 | td {
408 | text-align: left;
409 | padding: 4px;
410 | }
411 |
412 | .invisible {
413 | opacity: 0;
414 | }
415 |
416 | .location-field:focus-within,
417 | .email-options:focus-within,
418 | .email-options:focus
419 |
420 | /* the radio btns field is different */
421 | .email-address:focus,
422 | .email-address:focus-within {
423 | border: 1px solid #ffffff55;
424 | }
425 |
426 | .you-signed-in-as .friend {
427 | width: fit-content;
428 | }
429 |
430 | .you-signed-in-as {
431 | display: inline-flex;
432 | }
433 |
434 | .field .edit-icon {
435 | fill: white;
436 | height: 13px;
437 | margin-left: -36px;
438 | margin-top: 15px;
439 | position: absolute;
440 | opacity: 0.5;
441 | }
442 |
443 | .field {
444 | font-size: 0.95em;
445 | }
446 |
447 | .mapbox-container {
448 | margin: auto;
449 | width: 110px;
450 | height: 110px;
451 | margin: 0;
452 | margin-top: 10px;
453 | background: #ffffff11;
454 | overflow: hidden;
455 | background: #9dc7d9;
456 | border-radius: 100px;
457 | }
458 |
459 | .mapbox-container .mapboxgl-canvas-container,
460 | .mapbox-container .mapboxgl-canvas-container canvas {
461 | border-radius: 100px;
462 | }
463 |
464 | .mapboxgl-canvas {
465 | position: relative !important;
466 | }
467 |
468 | .mapbox-container .mapboxgl-map {
469 | height: 100% !important;
470 | }
471 |
472 | /* pulsating background */
473 | @keyframes pulsingGreenBkgd {
474 | 0% {
475 | background-color: #1d7a42;
476 | }
477 |
478 | 50% {
479 | background-color: #013917;
480 | }
481 |
482 | 100% {
483 | background-color: #1d7a42;
484 | }
485 | }
486 |
487 | @keyframes pulsingSize {
488 | 0% {
489 | transform: scale(1);
490 | // font-size: 1em;
491 | // transform: translateX(0) translateY(0);
492 | }
493 |
494 | 50% {
495 | transform: scale(1.2);
496 | // font-size: 1.2em;
497 | // transform: translateX(-10px) translateY(-10px);
498 | }
499 |
500 | 100% {
501 | transform: scale(1);
502 | // font-size: 1em;
503 | // transform: translateX(0) translateY(0);
504 | }
505 | }
506 |
507 | .mapbox-container .center-point,
508 | .mapbox-container .center-point {
509 | width: 20px;
510 | height: 20px;
511 | font-size: 1.8em;
512 | background: radial-gradient(circle, rgba(255, 255, 255, 0.8) 0%, rgba(255, 255, 255, 0) 100%);
513 | // border: 1px solid white;
514 | // background-color: #e56800;
515 | border-radius: 100%;
516 | position: relative;
517 | left: calc(50%);
518 | top: calc(-50%);
519 | transform: translateX(-50%) translateY(-50%);
520 | // animation-name: pulsingSize;
521 | animation-duration: 3s;
522 | animation-iteration-count: infinite;
523 | }
524 |
525 | .location-fields {
526 | display: grid;
527 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
528 | gap: 16px;
529 | margin: 12px 0;
530 | width: 100%;
531 | }
532 |
533 | .location-field,
534 | .add-location-btn {
535 | background: #f6f6f6;
536 | border: 3px solid rgba(188, 181, 175, 0.15) !important;
537 | border-radius: 8px;
538 | margin: 0;
539 | display: flex;
540 | flex-direction: column;
541 | align-items: center;
542 | padding: 8px;
543 | border-radius: 8px;
544 | flex: 1;
545 | min-width: 200px;
546 | /* Adjust minimum width as needed */
547 | text-align: center;
548 | border: 1px solid #000;
549 | padding: 10px;
550 | }
551 |
552 | .add-location-btn.empty {
553 | border: 3px dashed rgb(255, 181, 128) !important;
554 | background-color: rgb(255, 248, 243);
555 | }
556 |
557 | .location-field .delete-location-btn {
558 | align-self: end;
559 | margin: 4px;
560 | fill: #bbb;
561 | cursor: pointer;
562 | }
563 |
564 | .location-field .location-field-top {
565 | margin: -12px 0 0 0;
566 | }
567 |
568 | .location-field .location-field-bottom {
569 | margin: 16px 12px 0 12px;
570 | }
571 |
572 | .location-field .location-input {
573 | background: white;
574 | margin: 4px 0 8px 0;
575 | padding: 4px 8px;
576 | width: calc(100% - 24px);
577 | border: 3px solid rgba(188, 181, 175, 0.3) !important;
578 | transition: all 0.2s ease-in-out;
579 | border-radius: 8px;
580 | }
581 |
582 | .location-field .location-input[value='']:not(:focus) {
583 | background-color: rgb(255, 248, 243);
584 | border: 3px solid rgb(255, 181, 128) !important;
585 | transition: all 0.2s ease-in-out;
586 | }
587 |
588 | .location-field .location-input:focus {
589 | border: 3px solid rgba(188, 181, 175, 0.5) !important;
590 | transition: all 0.2s ease-in-out;
591 | border-radius: 8px;
592 | }
593 |
594 | .location-field .location-type-radio {
595 | width: 100%;
596 | text-align: left;
597 | }
598 |
599 | .location-field .location-type-radio input {
600 | margin: 6px;
601 | cursor: pointer;
602 | // display: none;
603 | }
604 |
605 | .location-field .location-type-radio label {
606 | margin: 3px;
607 | padding: 4px 8px;
608 | border-radius: 3px;
609 | line-height: 2.2em;
610 | cursor: pointer;
611 | color: #013917c0;
612 | transition: color 0.1s ease-in-out;
613 | }
614 |
615 | .location-field .location-type-radio label:hover,
616 | .location-field .location-type-radio input:hover+label {
617 | color: #013917e0;
618 | transition: color 0.1s ease-in-out;
619 | }
620 |
621 | .add-location-btn {
622 | padding: 24px 10px;
623 | min-height: 264px;
624 | color: #013917c0;
625 | background: white;
626 | border: 3px dashed rgba(188, 181, 175, 0.4) !important;
627 | border-radius: 8px;
628 | cursor: pointer;
629 | transition: all 0.2s ease-in-out;
630 | font-weight: bolder;
631 | display: inline-flex;
632 | align-items: center;
633 | justify-content: center;
634 | }
635 |
636 | .add-location-btn:hover {
637 | color: #013917;
638 | border: 3px dashed rgba(188, 181, 175, 0.8) !important;
639 | transition: all 0.2s ease-in-out;
640 | }
641 |
642 | .ready-for-review {
643 | position: fixed;
644 | background: white;
645 | border-top: 2px solid rgb(229, 225, 220);
646 | background: rgb(235, 232, 229);
647 | padding: 16px 16px 24px 16px;
648 | box-shadow: 0 0 8px 0 rgba(255, 255, 255, 1);
649 | left: 0;
650 | right: 0;
651 | bottom: 0;
652 | display: flex;
653 | flex-direction: column-reverse;
654 | align-items: center;
655 | justify-content: center;
656 | flex-wrap: wrap-reverse;
657 | gap: 12px;
658 | }
659 |
660 | .ready-for-review button {
661 | color: white;
662 | background: #013917e0;
663 | border-radius: 8px;
664 | padding: 12px 16px;
665 | font-weight: bold;
666 | cursor: pointer;
667 | transition: all 0.2s ease-in-out;
668 | width: fit-content;
669 | min-width: 200px;
670 | }
671 |
672 | .ready-for-review button:hover {
673 | background: #013917;
674 | transition: all 0.2s ease-in-out;
675 | }
676 |
677 | .ready-for-review button.disabled {
678 | background: rgba(160, 160, 160, 0.3);
679 | color: rgba(160, 160, 160, 0.9);
680 | cursor: not-allowed;
681 | }
682 |
683 | .ready-for-review .errors-list {
684 | // width: 336px;
685 | text-wrap: balance;
686 | line-height: 1.5em;
687 | font-size: 0.9em;
688 | text-align: center;
689 | }
690 |
691 | @media (min-width: 570px) {
692 | .ready-for-review .errors-list {
693 | text-align: left;
694 | }
695 | }
696 |
697 | .ready-for-review .errors-list ul {
698 | margin-top: 6px;
699 | }
700 |
701 | .welcome-message {
702 | line-height: 1.8em;
703 | // text-wrap: balance;
704 | display: flex;
705 | align-items: center;
706 | justify-content: space-around;
707 | gap: 24px;
708 | background: #f6f6f6;
709 | background: rgb(235, 232, 229, 0.6);
710 | padding: 24px 48px 24px 32px;
711 | border-radius: 6px;
712 | }
713 |
714 | /**** image upload form ****************************************************************************/
715 |
716 | .img-upload input[type='file'] {
717 | display: block;
718 | border: 2px dashed #ccc;
719 | text-align: center;
720 | transition: border-color 0.3s ease;
721 | cursor: pointer !important;
722 | }
723 |
724 | .img-upload input[type='file'] {
725 | // border: 3px solid rgba(188, 181, 175, 0.3);
726 | background: rgba(188, 181, 175, 0.2);
727 | width: 80%;
728 | padding: 48px;
729 | margin: 16px 0 8px 0;
730 | border-radius: 8px;
731 | }
732 |
733 | .img-upload.empty input[type='file'] {
734 | border: 3px dashed rgb(255, 181, 128) !important;
735 | background-color: rgb(255, 248, 243);
736 | }
737 |
738 | .img-upload p.error {
739 | color: rgb(255, 106, 0);
740 | margin-bottom: 8px;
741 | }
742 |
743 | // TODO: hmm this isn't working...
744 | #file-upload-button {
745 | margin-right: 8px;
746 | border-radius: 8px;
747 | font-family: 'Roboto Mono', monospace;
748 | border: 3px solid green;
749 | }
750 |
751 | .bio-row .picture {
752 | display: inline-block !important;
753 | }
754 |
755 | .bio-row .picture img {
756 | // height: 200px;
757 | // width: 100px;
758 | // background: pink;
759 | max-height: 200px;
760 | max-width: 100%;
761 | margin: 8px 8px 0 0;
762 | border-radius: 8px;
763 | background: #ccc;
764 | border: 1px solid #ccc;
765 | border-radius: 8px;
766 | }
767 |
768 | .bio-row .picture .cancel-btn {
769 | margin-left: -22px;
770 | margin-top: 4px;
771 | float: right;
772 | cursor: pointer;
773 | }
774 |
775 | .bio-row .picture svg.cancel-icon {
776 | transform: scale(1.1);
777 | border-radius: 8px;
778 | transition: all 100ms ease-in-out;
779 | fill: #718371;
780 | background: white;
781 | border: 1px solid white;
782 | margin-left: -20px;
783 | box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.3);
784 | }
785 |
786 | .bio-row .picture svg.cancel-icon:hover {
787 | fill: #3b5f46;
788 | transition: all 100ms ease-in-out;
789 | transform: scale(1.18);
790 | }
791 |
792 | // .img-upload input[type='submit'] {
793 | // display: block;
794 | // width: 100%;
795 | // padding: 10px;
796 | // background-color: #4caf50;
797 | // color: white;
798 | // border: none;
799 | // border-radius: 8px;
800 | // cursor: pointer;
801 | // font-size: 16px;
802 | // }
803 |
804 | // .img-upload input[type='submit']:hover {
805 | // background-color: #45a049;
806 | // }
--------------------------------------------------------------------------------