├── system.properties ├── resources └── public │ ├── css │ ├── custom.css │ ├── admin.css │ └── coming-soon.css │ └── img │ └── logo.png ├── examples ├── ideaferret │ ├── img │ │ └── ferret.jpg │ ├── css │ │ └── custom.css │ └── config.edn ├── silver_bullet │ ├── img │ │ └── logo.png │ └── config.edn └── falklandsophile │ ├── img │ └── falklands.jpg │ └── config.edn ├── test ├── coming_soon │ ├── lib │ │ └── check.clj │ ├── unit │ │ └── email.clj │ └── features │ │ ├── models │ │ ├── retrieve_contact.feature │ │ ├── list_contacts.feature │ │ ├── remove_contact.feature │ │ └── add_contact.feature │ │ └── step_definitions │ │ └── models │ │ └── contact_steps.clj.old └── test-config.edn ├── .lein-spell ├── .gitignore ├── src └── coming_soon │ ├── models │ ├── email.cljc │ └── contact.clj │ ├── webhooks │ ├── webhook.clj │ ├── posthere_io.clj │ └── mailchimp.clj │ ├── config.clj │ ├── templates │ ├── analytics.html │ ├── admin.html │ └── home.html │ ├── lib │ ├── redis.clj │ ├── colors.clj │ └── webhooks.clj │ ├── controllers │ ├── contacts.clj │ ├── redis.clj │ └── admin.clj │ ├── views │ ├── admin.clj │ └── contacts.clj │ ├── app.clj │ └── cljs │ └── coming_soon.cljs ├── REPL_prep.clj ├── .travis.yml ├── CHANGELOG.md ├── config.edn ├── project.clj ├── LICENSE.txt └── README.md /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=1.8 -------------------------------------------------------------------------------- /resources/public/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Put your customized CSS styles here */ 2 | -------------------------------------------------------------------------------- /resources/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnootyMonkey/coming-soon/HEAD/resources/public/img/logo.png -------------------------------------------------------------------------------- /examples/ideaferret/img/ferret.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnootyMonkey/coming-soon/HEAD/examples/ideaferret/img/ferret.jpg -------------------------------------------------------------------------------- /examples/silver_bullet/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnootyMonkey/coming-soon/HEAD/examples/silver_bullet/img/logo.png -------------------------------------------------------------------------------- /examples/falklandsophile/img/falklands.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnootyMonkey/coming-soon/HEAD/examples/falklandsophile/img/falklands.jpg -------------------------------------------------------------------------------- /test/coming_soon/lib/check.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.lib.check 2 | (:require [clojure.test :refer (is)])) 3 | 4 | (defmacro check [forms] 5 | `(assert (is ~forms))) -------------------------------------------------------------------------------- /.lein-spell: -------------------------------------------------------------------------------- 1 | analytics 2 | buildpack 3 | cljc 4 | cljsbuild 5 | dynos 6 | integrations 7 | midje 8 | myredis 9 | nano 10 | openredis 11 | redis 12 | rediscloud 13 | redisgreen 14 | redistogo 15 | resellers 16 | screenshot 17 | signups 18 | toolbelt 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /out 3 | /repl 4 | /target 5 | /targets 6 | /lib 7 | /classes 8 | /checkouts 9 | /bin 10 | pom.xml 11 | *.jar 12 | *.class 13 | .lein-* 14 | .nrepl-* 15 | 16 | resources/public/js/coming_soon.js 17 | resources/public/img/falklands.jpg 18 | resources/public/img/ferret.jpg -------------------------------------------------------------------------------- /src/coming_soon/models/email.cljc: -------------------------------------------------------------------------------- 1 | (ns coming-soon.models.email) 2 | 3 | ;; Regex for a valid email address 4 | (def email-regex #"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?") 5 | 6 | (defn valid? 7 | "Determine if the specified email address is valid according to our email regex." 8 | [email] 9 | (and (not (nil? email)) (re-matches email-regex email))) -------------------------------------------------------------------------------- /resources/public/css/admin.css: -------------------------------------------------------------------------------- 1 | div#main-container { 2 | margin-top: 50px; 3 | padding: 35px; 4 | text-align: center; 5 | -webkit-border-radius: 10px; 6 | -moz-border-radius: 10px; 7 | border-radius: 10px; 8 | } 9 | 10 | table.contacts th { 11 | text-align: center !important; 12 | } 13 | 14 | table.contacts th.number { 15 | text-align: left !important; 16 | } 17 | 18 | table.contacts td.number { 19 | text-align: left !important; 20 | } -------------------------------------------------------------------------------- /src/coming_soon/webhooks/webhook.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.webhooks.webhook) 2 | 3 | ;; Call an external service as a result of a new contact (:email and 4 | ;; :referrer) being created. The webhook specific configuration options, if any, 5 | ;; are passed in as a :config map. It's OK if your implementation is blocking as 6 | ;; this will be invoked asynchronously. 7 | (defmulti invoke :webhook) 8 | 9 | (defmethod invoke :default [args] 10 | (println "Warning: no webhook implementation for -" (:webhook args))) -------------------------------------------------------------------------------- /REPL_prep.clj: -------------------------------------------------------------------------------- 1 | ;; productive set of development namespaces 2 | (use 'clojure.stacktrace) 3 | (use 'clj-time.format) 4 | (use 'print.foo) 5 | (require '[coming-soon.config :as config] :reload-all) 6 | (require '[coming-soon.models.email :as email] :reload-all) 7 | (require '[coming-soon.models.contact :as contact] :reload-all) 8 | 9 | ;; Try out configured webhooks 10 | (require '[coming-soon.lib.webhooks :as wh] :reload-all) 11 | (wh/call "foo@bar.com" "http://cnn.com/") 12 | 13 | ;; print last exception 14 | (print-stack-trace *e) -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | jdk: 3 | - oraclejdk8 4 | lein: 2.7.1 5 | services: 6 | - redis-server 7 | cache: 8 | directories: 9 | - "$HOME/.m2" 10 | script: 11 | - lein2 cljsbuild once 12 | - lein2 test! 13 | - lein2 eastwood 14 | - lein2 kibit 15 | branches: 16 | only: 17 | - master 18 | - dev 19 | notifications: 20 | slack: 21 | secure: Xymsv6anNyxB6t79IchZQxjMk14huMEQ1FXCxNRwSmfHzqeRPEvLTEv9JWd6T4xoaoW7a1H5RV5lEyRb9iPV9qE1JVpE22AaZmuxvgLl1kAN1X6LN1vTqN5HCrPGQCuHY0HFbhV0/wewGB3fU02DaXO0qxp7qMxxoH5Z1AJJGfQ= 22 | -------------------------------------------------------------------------------- /src/coming_soon/config.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.config 2 | (:require [environ.core :refer (env)])) 3 | 4 | (defonce config-file (or (env :config-file) "config.edn")) 5 | 6 | (defonce config (read-string (slurp config-file))) 7 | 8 | (defonce coming-soon (config :coming-soon)) 9 | 10 | (defonce admin-user (or (env :admin-user) (coming-soon :admin-user))) 11 | 12 | (defonce admin-password (or (env :admin-password) (coming-soon :admin-password))) 13 | 14 | (defonce redis (config :redis)) 15 | 16 | (defonce landing-page (config :landing-page)) 17 | 18 | (defonce webhooks (config :webhooks)) -------------------------------------------------------------------------------- /src/coming_soon/webhooks/posthere_io.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.webhooks.posthere-io 2 | (:require [clj-http.client :as client] 3 | [clj-json.core :as json] 4 | [coming-soon.webhooks.webhook :as webhook])) 5 | 6 | (def posthere-io-url "http://posthere.io/") 7 | 8 | (defmethod webhook/invoke :posthere-io [args] 9 | (let [url (str posthere-io-url (:posthere-io-uuid (:config args)))] 10 | (println "Webhook callback posting to:" url) 11 | (client/post url { 12 | :content-type :json 13 | :body (json/generate-string { 14 | :email (:email args) 15 | :referrer (:referrer args)})}))) -------------------------------------------------------------------------------- /src/coming_soon/templates/analytics.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/coming_soon/lib/redis.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.lib.redis 2 | (:require [taoensso.carmine :as car] 3 | [coming-soon.config :as config] 4 | [clojure.string :refer (blank?)])) 5 | 6 | ;; Redis config 7 | (defn- conn-spec 8 | "Determine if we should connect with a Redis URL, and if so, which one" 9 | [] 10 | (let [url (config/redis :redis-connect-URL) 11 | env (config/redis :redis-env-variable)] 12 | (cond 13 | (not (blank? url)) {:uri url} 14 | (not (blank? env)) {:uri (get (System/getenv) env)} 15 | :else {}))) 16 | 17 | ;; A Redis connection with Carmine 18 | (def redis-conn {:pool {} :spec (conn-spec)}) 19 | (defmacro with-car [& body] `(car/wcar redis-conn ~@body)) 20 | 21 | ;; Instance specific namespace 22 | (def prefix (config/coming-soon :instance-prefix)) -------------------------------------------------------------------------------- /examples/ideaferret/css/custom.css: -------------------------------------------------------------------------------- 1 | /* Put your customized CSS styles here */ 2 | div#main-container { 3 | margin-top: 50px; 4 | padding: 0px; 5 | max-width: 10000px; 6 | text-align: left;} 7 | 8 | div#social { 9 | float: right; 10 | margin-top: -455px; 11 | padding-right: 30px; 12 | } 13 | 14 | p#app-summary {margin: 0 0 0 0;} 15 | 16 | h3.instructions {margin-top: 50px;} 17 | 18 | input#email { 19 | border: 0px solid; 20 | box-shadow: none; 21 | height: 60px; 22 | width: 320px; 23 | font-size: 1.7em;} 24 | 25 | form { 26 | border-top: 1px solid; 27 | border-bottom: 1px solid; 28 | margin: 0 0 0 0; 29 | width: 470px; 30 | } 31 | 32 | button.btn { 33 | height: 50px; 34 | width: 130px; 35 | font-size: 1.6em; 36 | } 37 | 38 | p#app-summary {font-size: 1.5em; line-height: 1.6em;} 39 | 40 | span.sum {color: #853124;} -------------------------------------------------------------------------------- /src/coming_soon/webhooks/mailchimp.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.webhooks.mailchimp 2 | (:require [clj-http.client :as client] 3 | [clj-json.core :as json] 4 | [environ.core :refer (env)] 5 | [coming-soon.config :as config] 6 | [coming-soon.webhooks.webhook :as webhook])) 7 | 8 | (def mailchimp-api "https://us11.api.mailchimp.com/3.0/lists/") 9 | 10 | (defonce api-key (or (env :mailchimp-api-key) (get-in config/webhooks [:mailchimp :api-key]))) 11 | 12 | (defonce list-id (get-in config/webhooks [:mailchimp :list-id])) 13 | (defonce status (get-in config/webhooks [:mailchimp :status])) 14 | 15 | (defmethod webhook/invoke :mailchimp [args] 16 | (let [url (str mailchimp-api list-id "/members")] 17 | (println "Webhook callback posting to:" url) 18 | (client/post url { 19 | :content-type :json 20 | :basic-auth ["coming-soon" api-key] 21 | :body (json/generate-string { 22 | :email_address (:email args) 23 | :status status})}))) -------------------------------------------------------------------------------- /src/coming_soon/controllers/contacts.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.controllers.contacts 2 | (:require [clojure.string :as s] 3 | [compojure.core :refer (defroutes GET POST)] 4 | [coming-soon.models.contact :as contact] 5 | [coming-soon.models.email :as email] 6 | [coming-soon.views.contacts :as view] 7 | [coming-soon.lib.webhooks :as webhooks])) 8 | 9 | (defn- create-req [email referrer] 10 | (if-not (email/valid? email) 11 | {:status 403 :body (pr-str {:contact "invalid"})} 12 | (if (contact/create email referrer) 13 | (do 14 | (webhooks/call email referrer) ; invoke configured webhooks asynchronously 15 | {:status 200 :body (pr-str {:contact "created"})}) 16 | {:status 500 :body (pr-str {:contact "error"})}))) 17 | 18 | (defroutes contact-routes 19 | (GET "/" [:as {headers :headers}] 20 | (s/join (view/home-page (get headers "referer" "")))) 21 | (POST "/subscribe" [email referrer] 22 | (create-req email referrer))) -------------------------------------------------------------------------------- /src/coming_soon/views/admin.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.views.admin 2 | (:require [net.cgrand.enlive-html :refer :all] 3 | [clj-time.format :refer (formatter with-zone parse unparse)] 4 | [clj-time.core :refer (to-time-zone default-time-zone)] 5 | [coming-soon.models.contact :refer (timestamp-format)] 6 | [coming-soon.config :refer (landing-page)])) 7 | 8 | (def output-format (with-zone (formatter "M-d-yyyy h:mm a") (default-time-zone))) 9 | 10 | (deftemplate admin-page "coming_soon/templates/admin.html" [contacts] 11 | [:#app-title] (html-content (landing-page :app-title)) 12 | [:table.contacts :tbody :tr] (clone-for [contact contacts] 13 | [:tr first-child] (content (str (:id contact))) 14 | [:tr (nth-child 2)] (content (:email contact)) 15 | [:tr (nth-child 3)] (content (html [:a {:href (:referrer contact) :target "_new"} (:referrer contact)])) 16 | [:tr (nth-child 4)] (content (unparse output-format (to-time-zone (parse timestamp-format (:updated-at contact)) (default-time-zone)))))) -------------------------------------------------------------------------------- /src/coming_soon/app.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.app 2 | (:gen-class) 3 | (:require [compojure.core :refer (defroutes ANY)] 4 | [ring.middleware.params :refer (wrap-params)] 5 | [ring.adapter.jetty :as ring] 6 | [ring.middleware.basic-authentication :refer (wrap-basic-authentication)] 7 | [coming-soon.controllers.contacts :as contacts] 8 | [coming-soon.controllers.admin :as admin] 9 | [coming-soon.controllers.redis :as redis] 10 | [compojure.route :as route])) 11 | 12 | (defroutes app-routes 13 | contacts/contact-routes 14 | redis/test-routes 15 | (ANY "/contacts*" [] 16 | (wrap-basic-authentication admin/admin-routes admin/authenticated?)) 17 | (route/resources "/") 18 | (route/not-found "Page Not Found")) 19 | 20 | (def app (wrap-params app-routes)) 21 | 22 | (defn start [port] 23 | (ring/run-jetty app {:port port :join? false})) 24 | 25 | (defn -main [] 26 | (let [port (Integer/parseInt 27 | (or (System/getenv "PORT") "3000"))] 28 | (start port))) -------------------------------------------------------------------------------- /src/coming_soon/controllers/redis.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.controllers.redis 2 | (:require [compojure.core :refer (defroutes GET)] 3 | [hiccup.core :refer (html)] 4 | [coming-soon.lib.redis :refer (redis-conn)] 5 | [taoensso.carmine :as car])) 6 | 7 | (def head (html [:head [:title "Redis Test"]])) 8 | 9 | (defn- response-content [status] 10 | (let [ 11 | color (if status "green" "red") 12 | label (if status "OK" "BORKED")] 13 | (html [:html head [:body "Connection to Redis is: " 14 | [:span {:style (str "font-weight: bold;color:" color ";")} label]]]))) 15 | 16 | (def redis-ok {:status 200 :body (response-content true)}) 17 | (def redis-borked {:status 500 :body (response-content false)}) 18 | 19 | (defn- redis-test [] 20 | (try 21 | (let [response 22 | (car/wcar redis-conn (car/ping))] 23 | (if (= response "PONG") 24 | redis-ok 25 | redis-borked)) 26 | (catch Exception e redis-borked))) 27 | 28 | (defroutes test-routes 29 | (GET "/redis-test" [] (redis-test))) -------------------------------------------------------------------------------- /src/coming_soon/lib/colors.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.lib.colors 2 | (:require [clojure.contrib.string :refer (map-str)] 3 | [tinter.core :refer (hex-str-to-dec)])) 4 | 5 | (defn- rgb-tuple [hex-color] 6 | ;; strip off the prefixed # if there is one 7 | (let [color (last (clojure.string/split hex-color #"#"))] 8 | (let [full-color 9 | ;; convert XYZ to XXYYZZ if necessary 10 | (if (= (count color) 3) (map-str #(str % %) color) color)] 11 | ;; convert the 6 hex digits to a sequence of the 3 RGB colors 12 | (hex-str-to-dec full-color)))) 13 | 14 | (defn rgb-color 15 | "Given a hex CSS color such as #EEE or #1A1A1A, convert it to an RGB CSS color such as rgb(255, 16, 22)" 16 | [hex-color] 17 | (str "rgb(" (clojure.string/join "," (rgb-tuple hex-color)) ")")) 18 | 19 | (defn rgba-color 20 | "Given a hex CSS color such as #EEE or #1A1A1A and an opacity, convert it to an RGB CSS color such as rgb(255, 16, 22, 0.5)" 21 | [hex-color alpha] 22 | (str "rgba(" (clojure.string/join "," (rgb-tuple hex-color)) "," alpha ")")) -------------------------------------------------------------------------------- /src/coming_soon/lib/webhooks.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.lib.webhooks 2 | (:require [clojure.core.async :as a :refer [chan sliding-buffer put! go-loop truthy) 10 | ?address 11 | "email@example.com" 12 | "firstname.lastname@example.com" 13 | "email@subdomain.example.com" 14 | "firstname+lastname@example.com" 15 | "1234567890@example.com" 16 | "email@example-one.com" 17 | "_______@example.com" 18 | "email@example.name" 19 | "email@example.museum" 20 | "email@example.co.jp" 21 | "firstname-lastname@example.com") 22 | 23 | (tabular (fact "invalid email addresses are determined to be invalid" 24 | (valid? ?address) => falsey) 25 | ?address 26 | "plainaddress" 27 | "#@%^%#$@#$@#.com" 28 | "@example.com" 29 | "Joe Smith " 30 | "email.example.com" 31 | "email@example@example.com" 32 | ".email@example.com" 33 | "email.@example.com" 34 | "email..email@example.com" 35 | "あいうえお@example.com" 36 | "email@example.com (Joe Smith)" 37 | "email@example" 38 | "email@-example.com" 39 | "email@example..com" 40 | "Abc..123@example.com") -------------------------------------------------------------------------------- /resources/public/css/coming-soon.css: -------------------------------------------------------------------------------- 1 | /* Page structure - Sticky footer details at http://www.martinbean.co.uk/bootstrap/sticky-footer/ */ 2 | 3 | html,body { 4 | height: 100%; 5 | } 6 | 7 | #wrap { 8 | min-height: 100%; 9 | height: auto !important; 10 | height: 100%; 11 | margin: 0 auto -80px; 12 | } 13 | 14 | div#main-container { 15 | margin-top: 50px; 16 | padding: 35px; 17 | max-width: 650px; 18 | text-align: center; 19 | -webkit-border-radius: 10px; 20 | -moz-border-radius: 10px; 21 | border-radius: 10px; 22 | } 23 | 24 | span#thank-you { 25 | display: none; 26 | } 27 | 28 | span#error-message { 29 | display: none; 30 | } 31 | 32 | #push { 33 | height: 100px; /* adjust the size of push as need be to keep your footer below your content */ 34 | } 35 | 36 | #footer { 37 | height: 80px; 38 | } 39 | 40 | /* Page visual details */ 41 | 42 | img.logo { 43 | vertical-align: baseline; 44 | margin-right: 5px; 45 | } 46 | 47 | h1 { 48 | font-size: 5.5em; 49 | line-height: 1em; 50 | } 51 | 52 | h2 { 53 | font-size: 2.5em; 54 | line-height: 1.5em; 55 | } 56 | 57 | h3 { 58 | font-weight: normal; 59 | font-size: 1.5em; 60 | } 61 | 62 | p#app-summary { 63 | margin: 0 30px 0 30px; 64 | } 65 | 66 | p#spam-msg { 67 | font-style: italic; 68 | } 69 | 70 | div#social { 71 | margin-top: 20px; 72 | text-align: center; 73 | } 74 | 75 | ul#social-links li { 76 | margin: 0 10px 0 10px; 77 | display: inline; 78 | list-style: none; 79 | } 80 | 81 | div#social a { 82 | text-decoration: none; 83 | } 84 | 85 | div#footer { 86 | text-align: center; 87 | } -------------------------------------------------------------------------------- /test/coming_soon/features/models/retrieve_contact.feature: -------------------------------------------------------------------------------- 1 | Feature: Retrieving Contacts 2 | 3 | Background: 4 | Given the system knows about the following contacts: 5 | |email |referrer | 6 | |zuck@facebook.com |http://facebook.com/cool-stuff | 7 | |obama@whitehouse.gov |http://cia.gov/secret-stuff | 8 | |jonny@apple.com | | 9 | And the contact count is 3 10 | And the content store is sane 11 | 12 | Scenario: Retrieving a contact by email 13 | When I retrieve the contact for "zuck@facebook.com" the "id" is "1" 14 | When I retrieve the contact for "zuck@facebook.com" the "referrer" is "http://facebook.com/cool-stuff" 15 | When I retrieve the contact for "obama@whitehouse.gov" the "id" is "2" 16 | When I retrieve the contact for "obama@whitehouse.gov" the "referrer" is "http://cia.gov/secret-stuff" 17 | When I retrieve the contact for "jonny@apple.com" the "id" is "3" 18 | When I retrieve the contact for "jonny@apple.com" the "referrer" is blank 19 | 20 | Scenario: Retrieving a non-existing contact by email 21 | When I retrieve the contact for "zuck@faceboo.com" it doesn't exist 22 | 23 | Scenario: Retrieving a contact by id 24 | When I retrieve the contact for id 1 the "email" is "zuck@facebook.com" 25 | When I retrieve the contact for id 1 the "referrer" is "http://facebook.com/cool-stuff" 26 | When I retrieve the contact for id 2 the "email" is "obama@whitehouse.gov" 27 | When I retrieve the contact for id 2 the "referrer" is "http://cia.gov/secret-stuff" 28 | When I retrieve the contact for id 3 the "email" is "jonny@apple.com" 29 | When I retrieve the contact for id 3 the "referrer" is blank 30 | 31 | Scenario: Retrieving a non-existing contact by id 32 | When I retrieve the contact for id 4 it doesn't exist -------------------------------------------------------------------------------- /test/coming_soon/features/models/list_contacts.feature: -------------------------------------------------------------------------------- 1 | Feature: Listing Contacts 2 | 3 | Background: 4 | Given there are no contacts 5 | And the contact count is 0 6 | And I add a contact for "zuck@facebook.com" with a referrer from "http://facebook.com/cool-stuff" 7 | And I delay a moment 8 | And I add a contact for "obama@whitehouse.gov" with a referrer from "http://cia.gov/secret-stuff" 9 | And I delay a moment 10 | And I add a contact for "biden@whitehouse.gov" with a referrer from "http://cia.gov/secret-stuff" 11 | And I delay a moment 12 | And I add a contact for "jonny@apple.com" 13 | And now the contact count is 4 14 | And the content store is sane 15 | 16 | Scenario: Listing all contacts in collected at order 17 | When I list all contacts 18 | Then the list contains 4 items 19 | And the next contact is "zuck@facebook.com" with the id 1 and the referrer "http://facebook.com/cool-stuff" 20 | And the next contact is "obama@whitehouse.gov" with the id 2 and the referrer "http://cia.gov/secret-stuff" 21 | And the next contact is "biden@whitehouse.gov" with the id 3 and the referrer "http://cia.gov/secret-stuff" 22 | And the next contact is "jonny@apple.com" with the id 4 and no referrer 23 | 24 | Scenario: Listing all emails in collected at order 25 | When I list all emails 26 | Then the list contains 4 items 27 | And the next email is "zuck@facebook.com" 28 | And the next email is "obama@whitehouse.gov" 29 | And the next email is "biden@whitehouse.gov" 30 | And the next email is "jonny@apple.com" 31 | 32 | Scenario: Listing all referrers in referral count order 33 | When I list all referrals 34 | Then the list contains 2 items 35 | And the next referrer is "http://cia.gov/secret-stuff" with a count of 2 36 | And the next referrer is "http://facebook.com/cool-stuff" with a count of 1 -------------------------------------------------------------------------------- /test/coming_soon/features/models/remove_contact.feature: -------------------------------------------------------------------------------- 1 | Feature: Removing Contacts 2 | 3 | Background: 4 | Given the system knows about the following contacts: 5 | |email |referrer | 6 | |zuck@facebook.com |http://facebook.com/cool-stuff | 7 | |obama@whitehouse.gov |http://cia.gov/secret-stuff | 8 | |jonny@apple.com |http://apple.com/pretty-stuff | 9 | And the contact count is 3 10 | And the content store is sane 11 | 12 | Scenario: Removing a contact by email 13 | When I remove the contact for "zuck@facebook.com" 14 | Then the contact count is 2 15 | And the contact "zuck@facebook.com" does not exist 16 | And the contact "obama@whitehouse.gov" exists 17 | And the contact "jonny@apple.com" exists 18 | And the content store is sane 19 | 20 | Scenario: Removing a contact by id 21 | When I remove the contact with id 2 22 | Then the contact count is 2 23 | And the contact "zuck@facebook.com" exists 24 | And the contact "obama@whitehouse.gov" does not exist 25 | And the contact "jonny@apple.com" exists 26 | And the content store is sane 27 | 28 | Scenario: Erasing all contacts 29 | When I erase all contacts 30 | Then the contact count is 0 31 | And the contact "zuck@facebook.com" does not exist 32 | And the contact "obama@whitehouse.gov" does not exist 33 | And the contact "jonny@apple.com" does not exist 34 | And the content store is sane 35 | 36 | Scenario: Removing a contact that does not exist 37 | When I remove the contact for "zuck@facebook.co" 38 | Then the contact count is 3 39 | And the contact "zuck@facebook.com" exists 40 | And the contact "obama@whitehouse.gov" exists 41 | And the contact "jonny@apple.com" exists 42 | When I remove the contact with id 4 43 | Then the contact count is 3 44 | And the contact "zuck@facebook.com" exists 45 | And the contact "obama@whitehouse.gov" exists 46 | And the contact "jonny@apple.com" exists 47 | And the content store is sane -------------------------------------------------------------------------------- /src/coming_soon/templates/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | coming-soon Administration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
#emailreferrerwhen
1foo@bar.comhttp://bar.com/1/13/2013
49 |

Export: 50 | JSON, 51 | XML, 52 | CSV

53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com). 4 | 5 | ## v0.3.0-SNAPSHOT - [code](https://github.com/SnootyMonkey/posthere.io/compare/v0.2...HEAD) 6 | 7 | ### Added 8 | * Make async callbacks to invoke webhooks on successful signup (to services such as Slack and MailChimp) 9 | * MailChimp callback 10 | * POSThere.io callback 11 | * Provide admin's user name and password with ENV var 12 | * Added OpenCompany.io usage link and screenshot in README 13 | 14 | ### Changed 15 | * Show signups in newest first order on the admin page 16 | * Move from deprecated cross-overs to the new hotness, cljc 17 | * Upgraded the Bootstrap used in the default template to 3.3.5 18 | * Upgraded the Font Awesome used in the default template to 4.4.0 19 | * Upgraded Clojure to 1.9.0 20 | * Updated `build` alias to perform a complete production build 21 | * Updated dependencies 22 | 23 | ### Fixed 24 | * Minute in contact timestamp now has prefixed 0 on admin page 25 | * Added explicit AOT to remove leiningen uberjar warning 26 | * Updated the ClojureScript coming-soon namespace to remove warning 27 | * Google fonts are now pulled in with the current protocol (HTTP or HTTPS) 28 | 29 | ### Removed 30 | * Removed incomplete browser tests 31 | * Custom styling of admin page, without dedicated styles this was too often ugly and hard to read 32 | 33 | ## [v0.2.0](https://github.com/SnootyMonkey/coming-soon/releases/tag/v0.2.0) - 2015-05-14 - [code](https://github.com/SnootyMonkey/posthere.io/compare/v0.1...v0.2) 34 | 35 | ### Added 36 | * Added support for a title on all the user configured "social" links (Twitter, Facebook, GitHub, RSS) 37 | * Added unit tests and continuous integration with Travis CI 38 | * Added a configurable error message if saving user's contact info is not successful. :error and :error-color in your config.edn file 39 | 40 | ### Changed 41 | * Upgraded the Font Awesome used in the default template to 3.2.1 42 | * Moved expectations tests to midje 43 | * Updated dependencies 44 | 45 | ## [v0.1.0](https://github.com/SnootyMonkey/coming-soon/releases/tag/v0.1.0) - 2015-05-13 46 | 47 | Initial release. -------------------------------------------------------------------------------- /src/coming_soon/controllers/admin.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.controllers.admin 2 | (:require [clojure.string :as s] 3 | [compojure.core :refer (defroutes GET POST)] 4 | [clj-json.core :as json] 5 | [clojure.data.xml :as xml] 6 | [clojure-csv.core :as csv] 7 | [coming-soon.models.contact :refer (all-contacts)] 8 | [coming-soon.config :refer (admin-user admin-password)] 9 | [coming-soon.views.admin :as view])) 10 | 11 | (defn- headers [mime-type extension] 12 | { 13 | "Cache-Control" "must-revalidate" 14 | "Pragma" "must-revalidate" 15 | "Content-Type" (str mime-type ";charset=utf-8") 16 | "Content-Disposition" (str "attachment;filename=contacts." extension) 17 | }) 18 | 19 | (defn authenticated? [username password] 20 | (and 21 | (= username admin-user) 22 | (= password admin-password))) 23 | 24 | (defn- json-contacts [] 25 | {:status 200 26 | :headers (headers "application/json" "json") 27 | :body (json/generate-string (all-contacts))}) 28 | 29 | (defn- xml-contact 30 | "Return the specified contact as an XML element" 31 | [contact] 32 | (xml/element :contact {:id (str (:id contact))} [ 33 | (xml/element :email {} (:email contact)) 34 | (xml/element :referrer {} (:referrer contact)) 35 | (xml/element :updated-at {} (:updated-at contact))])) 36 | 37 | (defn- xml-contacts [] 38 | {:status 200 39 | :headers (headers "application/xml" "xml") 40 | :body (xml/emit-str (xml/element :contacts {} 41 | (map xml-contact (all-contacts))))}) 42 | 43 | (def csv-header ["id" "email" "referrer" "updated-at"]) 44 | 45 | (defn- csv-contact 46 | "Return the specified contact as a vector for CSV encoding" 47 | [contact] 48 | (list (str (:id contact)) (:email contact) (:referrer contact) (:updated-at contact))) 49 | 50 | (defn- csv-contacts [] 51 | {:status 200 52 | :headers (headers "text/csv" "csv") 53 | :body (csv/write-csv (cons csv-header (map csv-contact (all-contacts))))}) 54 | 55 | (defn- html-contacts [] 56 | (s/join (view/admin-page (all-contacts)))) 57 | 58 | (defroutes admin-routes 59 | (GET "/contacts" [] (html-contacts)) 60 | (GET "/contacts.html" [] (html-contacts)) 61 | (GET "/contacts.json" [] (json-contacts)) 62 | (GET "/contacts.xml" [] (xml-contacts)) 63 | (GET "/contacts.csv" [] (csv-contacts))) -------------------------------------------------------------------------------- /src/coming_soon/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 | 33 |
34 | 35 |

36 | 37 |

38 | 39 |

40 | 41 |

42 |

43 |

44 | 45 |
46 |
47 | 48 | 49 | 50 |
51 |
52 | 53 |

54 | 55 |
56 | 57 |
58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 | 66 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /test/test-config.edn: -------------------------------------------------------------------------------- 1 | { 2 | :coming-soon 3 | { 4 | :admin-user "admin" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_USER 5 | :admin-password "change_me!" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_PASSWORD 6 | :instance-prefix "test" ; make this unique if you are sharing a single redis instance w/ another coming-soon instance 7 | } 8 | :redis 9 | { 10 | ;; Local test instance 11 | } 12 | :landing-page 13 | { 14 | ;; Copy 15 | :page-title "Test Title" 16 | :app-title "Integration Test" 17 | :app-tagline "Test Tagline" 18 | :app-summary "Test Summary" ; OK to be blank 19 | :instructions "Test Instructions" 20 | :placeholder "Test Email Placeholder" ; OK to be blank 21 | :sign-up-btn "Test Button" 22 | :spam-msg "Test Spam Message" ; OK to be blank 23 | :thank-you "Test Thank You" 24 | :error "Test Error" 25 | :copyright "Test Copyright" ; OK to be blank 26 | 27 | ;; Fonts 28 | 29 | ;; see font advice at: 30 | ;; http://designshack.net/articles/css/10-great-google-font-combinations-you-can-copy/ 31 | ;; http://designshack.net/articles/typography/10-more-great-google-font-combinations-you-can-copy/ 32 | ;; http://www.insquaremedia.com/blog/15-web-design-stuff/50-our-favourite-google-font-combinations 33 | 34 | ;; see https://www.google.com/fonts for available fonts 35 | :app-title-font "Test Title Font" ; Google web font 36 | :app-title-backup-fonts "Test Title Backup Font" ; Web-safe fonts 37 | :body-font "Test Body Font" ; Google web font 38 | :body-backup-fonts "Test Body Backup Font" ; Web-safe fonts 39 | 40 | ;; Images 41 | :logo "/img/logo-test.png" ; OK to be blank 42 | :background-image "/img/bg-test.png" ; OK to be blank 43 | 44 | ;; Colors 45 | :background-color "#00000A" 46 | :container-bg-color "#0000A0" 47 | :container-opacity "0.5" ; 1 for solid, 0 for no container, 0.x for some transparency 48 | :app-title-color "#000A00" 49 | :app-tagline-color "#00A000" 50 | :app-summary-color "#0A0000" 51 | :instructions-color "#A00000" 52 | :spam-msg-color "#0000AA" 53 | :thank-you-color "#000AA0" 54 | :error-color "#00AA00" 55 | :copyright-color "#0AA000" 56 | :social-color "#AA0000" 57 | :social-hover-color "#000AAA" 58 | 59 | ;; Signup button 60 | :signup-btn-icon "fa-envelope-o" ; see http://fortawesome.github.io/Font-Awesome/cheatsheet/ OK to be blank 61 | :signup-btn-class "btn-test" ; see http://getbootstrap.com/css/#buttons 62 | 63 | ;; Social stuff 64 | :twitter-url "http://twitter.com/test" ; OK to be blank 65 | :twitter-title "Test Twitter Title" ; OK to be blank 66 | :facebook-url "https://www.facebook.com/test" ; OK to be blank 67 | :facebook-title "Test Facebook Title" ; OK to be blank 68 | :github-url "https://github.com/test" ; OK to be blank 69 | :github-title "Test GitHub Title" ; OK to be blank 70 | :blog-url "http://test.com/blog" ; OK to be blank 71 | :blog-title "Test Blog Title" ; OK to be blank 72 | :blog-feed "http://feeds.feedburner.com/test" ; OK to be blank 73 | 74 | ;; Analytics 75 | :analytics true ; include the analytics snippet in src/coming-soon/templates/analytics.html 76 | } 77 | } -------------------------------------------------------------------------------- /src/coming_soon/cljs/coming_soon.cljs: -------------------------------------------------------------------------------- 1 | (ns coming-soon.core 2 | (:require [cljs.reader :as reader] 3 | [clojure.string :as s] 4 | [goog.net.XhrIo :as xhr] 5 | [jayq.core :refer [$ document-ready bind val css attr remove-attr add-class remove-class]] 6 | [jayq.util :as util] 7 | [coming-soon.models.email :as email])) 8 | 9 | (def subscribe-url "/subscribe") 10 | 11 | ;; jQuery DOM lookups 12 | (def $backstretch ($ :.backstretch)) 13 | (def $instructions ($ :#instructions)) 14 | (def $thank-you ($ :#thank-you)) 15 | (def $error-message ($ :#error-message)) 16 | (def $email ($ :#email)) 17 | (def $submit ($ :#submit)) 18 | (def $submit-icon ($ :#submit-icon)) 19 | (def $subscribe ($ :#subscribe)) 20 | (def $referrer ($ :#referrer)) 21 | (def $spam-msg ($ :#spam-msg)) 22 | 23 | (defn log 24 | "log to the console only if we have a console (so not in old versions of IE)" 25 | [& msg] 26 | (if-not (= js/console js/undefined) 27 | (util/log (s/join msg)))) 28 | 29 | (defn- disable-submission [] 30 | ;; disable the submit button 31 | (attr $submit "disabled" "true") 32 | ;; spin the submit icon if there is one 33 | (if-not (nil? (val $submit-icon)) 34 | (add-class $submit-icon "icon-spin"))) 35 | 36 | (defn- enable-submission [] 37 | (remove-attr $submit "disabled") 38 | ;; stop spinning the submit icon if there is one 39 | (if-not (nil? (val $submit-icon)) 40 | (remove-class $submit-icon "icon-spin"))) 41 | 42 | (defn- update-for-success 43 | "remove the submission gadgetry and show the thank you message" 44 | [] 45 | ;; hide the email submission elements 46 | (css $email {:visibility "hidden"}) 47 | (css $submit {:visibility "hidden"}) 48 | (css $spam-msg {:visibility "hidden"}) 49 | ;; Hide the instructions and show the thank you 50 | (.hide $instructions) 51 | (.hide $error-message) 52 | (.show $thank-you)) 53 | 54 | (defn- update-for-failure 55 | "let the user know it's all gone pear-shaped" 56 | [] 57 | (log "Oh no! We've shat the bed.") 58 | (enable-submission) 59 | ;; Hide the instructions and show the error 60 | (.hide $instructions) 61 | (.show $error-message)) 62 | 63 | (defn success? 64 | "treat any 2xx status code as successful" 65 | [status] 66 | (if (and (>= status 200) (< status 300)) 67 | true 68 | false)) 69 | 70 | (defn receive-result 71 | "receive the server response to the email submission" 72 | [event] 73 | (let [status (.getStatus (.-target event))] 74 | (log "status " status) 75 | (log "received " (reader/read-string (.getResponseText (.-target event)))) 76 | (if (success? status) 77 | (update-for-success) 78 | (update-for-failure)))) 79 | 80 | (defn- submit 81 | "submit the provided email to the coming-soon server with AJAX" 82 | [email] 83 | (log "submitting " email) 84 | (xhr/send subscribe-url receive-result "POST" (str "email=" email "&referrer=" (val $referrer)))) 85 | 86 | (defn validate-email-and-submit 87 | "validate the provided email address and submit it if it's valid" 88 | [] 89 | (disable-submission) 90 | (let [email (val $email)] 91 | (log "in validate-email, email is: " email) 92 | ; IE doesn't support the HTML 5 email text input validation, so it could be blank 93 | (if (< (count email) 1) 94 | (do 95 | (enable-submission) 96 | (js/alert "Please provide your email address.")) 97 | ; IE doesn't support the HTML 5 email text input validation, so it could be invalid 98 | (if (email/valid? email) 99 | (submit email) 100 | (do 101 | (enable-submission) 102 | (js/alert "Please provide a valid email address."))))) 103 | false) ; prevent the form from submitting on its own 104 | 105 | (defn init 106 | "define the function to attach validate-email to the submit event of the form" 107 | [] 108 | (log "ClojureScript is working... that's good.") 109 | (bind $subscribe :submit validate-email-and-submit)) 110 | 111 | ;; initialize once the HTML page has loaded 112 | (document-ready init) -------------------------------------------------------------------------------- /test/coming_soon/features/models/add_contact.feature: -------------------------------------------------------------------------------- 1 | Feature: Adding Contacts 2 | 3 | The system should collect valid contacts so they can be notified later about important developments. 4 | 5 | Background: 6 | Given there are no contacts 7 | And the contact count is 0 8 | And the content store is sane 9 | 10 | Scenario: Adding a contact with an email 11 | When I add a contact for "zuck@facebook.com" 12 | Then the contact count is 1 13 | And the contact "obama@whitehouse.gov" does not exist 14 | And the contact "zuck@facebook.com" exists 15 | And the contact can be retrieved by "zuck@facebook.com" 16 | And the contact "zuck@facebook.com" has no referrer 17 | Then I delay a moment 18 | And the contact "zuck@facebook.com" has an updated-at of just before now 19 | And the content store is sane 20 | 21 | Scenario: Adding a Contact with an email and a referrer 22 | When I add a contact for "zuck@facebook.com" with a referrer from "http://facebook.com/cool-stuff" 23 | Then the contact count is 1 24 | And the contact "obama@whitehouse.gov" does not exist 25 | And the contact "zuck@facebook.com" exists 26 | And the contact can be retrieved by "zuck@facebook.com" 27 | And the contact "zuck@facebook.com" has a referrer of "http://facebook.com/cool-stuff" 28 | Then I delay a moment 29 | And the contact "zuck@facebook.com" has an updated-at of just before now 30 | And the content store is sane 31 | 32 | Scenario: Adding multiple contacts 33 | When I add a contact for "zuck@facebook.com" with a referrer from "http://facebook.com/cool-stuff" 34 | Then the contact count is 1 35 | And the contact "obama@whitehouse.gov" does not exist 36 | And the contact "zuck@facebook.com" exists 37 | And the contact can be retrieved by "zuck@facebook.com" 38 | And the contact "zuck@facebook.com" has a referrer of "http://facebook.com/cool-stuff" 39 | Then I delay a moment 40 | And the contact "zuck@facebook.com" has an updated-at of just before now 41 | Then I add a contact for "obama@whitehouse.gov" with a referrer from "http://cia.gov/secret-stuff" 42 | And the contact count is 2 43 | And the contact "zuck@facebook.com" exists 44 | And the contact can be retrieved by "zuck@facebook.com" 45 | And the contact "zuck@facebook.com" has a referrer of "http://facebook.com/cool-stuff" 46 | And the contact "obama@whitehouse.gov" exists 47 | And the contact can be retrieved by "obama@whitehouse.gov" 48 | And the contact "obama@whitehouse.gov" has a referrer of "http://cia.gov/secret-stuff" 49 | Then I delay a moment 50 | And the contact "obama@whitehouse.gov" has an updated-at of just before now 51 | And the contact "zuck@facebook.com" has an id before the contact "obama@whitehouse.gov" 52 | And the contact "zuck@facebook.com" and the contact "obama@whitehouse.gov" have unique ids 53 | And the contact "zuck@facebook.com" has an updated-at before the updated-at for contact "obama@whitehouse.gov" 54 | And the content store is sane 55 | 56 | Scenario: Adding the same contact multiple times 57 | When I add a contact for "zuck@facebook.com" with a referrer from "http://facebook.com/cool-stuff" 58 | Then the contact count is 1 59 | And the contact "obama@whitehouse.gov" does not exist 60 | And the contact "zuck@facebook.com" exists 61 | And the contact can be retrieved by "zuck@facebook.com" 62 | And the contact "zuck@facebook.com" has a referrer of "http://facebook.com/cool-stuff" 63 | Then I delay a moment 64 | And the contact "zuck@facebook.com" has an updated-at of just before now 65 | Then I add a contact for "zuck@facebook.com" with a referrer from "http://facebook.com/awesome-stuff" 66 | And the contact count is 1 67 | And the contact "obama@whitehouse.gov" does not exist 68 | And the contact "zuck@facebook.com" exists 69 | And the contact can be retrieved by "zuck@facebook.com" 70 | And the contact "zuck@facebook.com" has a referrer of "http://facebook.com/awesome-stuff" 71 | And the contact "zuck@facebook.com" has a more recent updated-at than before 72 | And the content store is sane 73 | 74 | Scenario: Don't add a contact with an invalid email 75 | When I add a contact for "zuck@facebook" with a referrer from "http://facebook.com/cool-stuff" 76 | Then the contact count is 0 77 | And the contact "zuck@facebook" does not exist 78 | And the content store is sane -------------------------------------------------------------------------------- /examples/falklandsophile/config.edn: -------------------------------------------------------------------------------- 1 | { 2 | :coming-soon 3 | { 4 | :admin-user "admin" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_USER 5 | :admin-password "change_me!" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_PASSWORD 6 | :instance-prefix "falklands" ; make this unique if you are sharing a single redis instance w/ another coming-soon instance 7 | } 8 | :redis 9 | { 10 | ;; If you're running Redis locally on the same machine and the default 6379 port, leave everything in this section commented out 11 | 12 | ;; HEROKU REDIS - uncomment the selected provider (remove the ;;) 13 | ;; :redis-env-variable "REDISCLOUD_URL" ; Comment in for Redis Cloud on Heroku 14 | ;; :redis-env-variable "REDISTOGO_URL" ; Comment in for Redis To Go on Heroku 15 | ;; :redis-env-variable "OPENREDIS_URL" ; Comment in for openredis on Heroku 16 | ;; :redis-env-variable "REDISGREEN_URL" ; Comment in for RedisGreen on Heroku 17 | 18 | ;; SELF HOSTED REDIS 19 | ;; A different port on the local machine might be: redis://localhost:7000/ 20 | ;; The default port, but on a different server might be: redis://10.1.0.70/ 21 | ;; Local on the default port, but with authentication might be: redis://admin:supersecret@localhost/ 22 | ;; Uncomment (remove the ;;) the line below for self-hosted redis that's not on port 6379 of localhost 23 | ;; :redis-connect-URL "redis://user:pass@host:port/" 24 | } 25 | :landing-page 26 | { 27 | ;; Copy 28 | :page-title "Falklandsophile | Launching soon!" 29 | :app-title "Falklandsophile" 30 | :app-tagline "Your complete guide to resources
about the Falkland Islands." 31 | :app-summary "" ; OK to be blank 32 | :instructions "Provide your email, and we'll let you know when it's ready." 33 | :placeholder "Email Address*" ; OK to be blank 34 | :sign-up-btn "Sign up" 35 | :spam-msg "* We won't spam you. We hate spam as much as you do." ; OK to be blank 36 | :thank-you "Thank you! We'll be in touch soon." 37 | :error "Sorry! Our system is temporarily down, please try again later." 38 | :copyright "© 2013-2017 Snooty Monkey, LLC" ; OK to be blank 39 | 40 | ;; Fonts 41 | 42 | ;; see font advice at: 43 | ;; http://designshack.net/articles/css/10-great-google-font-combinations-you-can-copy/ 44 | ;; http://designshack.net/articles/typography/10-more-great-google-font-combinations-you-can-copy/ 45 | ;; http://www.insquaremedia.com/blog/15-web-design-stuff/50-our-favourite-google-font-combinations 46 | 47 | ;; see https://www.google.com/fonts for available fonts 48 | :app-title-font "Dancing Script" ; Google web font 49 | :app-title-backup-fonts "Georgia, Times, serif" ; Web-safe fonts 50 | :body-font "Josefin Sans" ; Google web font 51 | :body-backup-fonts "Helvetica, Arial, sans-serif" ; Web-safe fonts 52 | 53 | ;; Images 54 | :logo "" ; OK to be blank 55 | :background-image "/img/falklands.jpg" ; OK to be blank 56 | 57 | ;; Colors 58 | :background-color "#675031" ; brown 59 | :container-bg-color "#EAFCFE" ; light green 60 | :container-opacity "0.9" ; 1 for solid, 0 for no container, 0.x for some transparency 61 | :app-title-color "#462A1C" ; dark brown 62 | :app-tagline-color "#462A1C" ; dark brown 63 | :app-summary-color "#462A1C" ; dark brown 64 | :instructions-color "#462A1C" ; dark brown 65 | :spam-msg-color "#646464" ; dark grey 66 | :thank-you-color "#63A687" ; greenish 67 | :error-color "#F00" ; red 68 | :copyright-color "#EAFCFE" ; light green 69 | :social-color "#96bfdb" ; sky blue 70 | :social-hover-color "#462a1c" ; dark brown 71 | 72 | ;; Signup button 73 | :signup-btn-icon "fa-envelope" ; see http://fortawesome.github.io/Font-Awesome/cheatsheet/ OK to be blank 74 | :signup-btn-class "btn-info" ; see http://getbootstrap.com/css/#buttons 75 | 76 | ;; Social stuff 77 | :twitter-url "" ; OK to be blank 78 | :twitter-title "" ; OK to be blank 79 | :facebook-url "" ; OK to be blank 80 | :facebook-title "" ; OK to be blank 81 | :github-url "" ; OK to be blank 82 | :github-title "" ; OK to be blank 83 | :blog-url "" ; OK to be blank 84 | :blog-title "" ; OK to be blank 85 | :blog-feed "" ; OK to be blank 86 | 87 | ;; Analytics 88 | :analytics false ; include the analytics snippet in src/coming-soon/templates/analytics.html 89 | } 90 | } -------------------------------------------------------------------------------- /examples/ideaferret/config.edn: -------------------------------------------------------------------------------- 1 | { 2 | :coming-soon 3 | { 4 | :admin-user "admin" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_USER 5 | :admin-password "change_me!" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_PASSWORD 6 | :instance-prefix "ferret" ; make this unique if you are sharing a single redis instance w/ another coming-soon instance 7 | } 8 | :redis 9 | { 10 | ;; If you're running Redis locally on the same machine and the default 6379 port, leave everything in this section commented out 11 | 12 | ;; HEROKU REDIS - uncomment the selected provider (remove the ;;) 13 | ;; :redis-env-variable "REDISCLOUD_URL" ; Comment in for Redis Cloud on Heroku 14 | ;; :redis-env-variable "REDISTOGO_URL" ; Comment in for Redis To Go on Heroku 15 | ;; :redis-env-variable "OPENREDIS_URL" ; Comment in for openredis on Heroku 16 | ;; :redis-env-variable "REDISGREEN_URL" ; Comment in for RedisGreen on Heroku 17 | 18 | ;; SELF HOSTED REDIS 19 | ;; A different port on the local machine might be: redis://localhost:7000/ 20 | ;; The default port, but on a different server might be: redis://10.1.0.70/ 21 | ;; Local on the default port, but with authentication might be: redis://admin:supersecret@localhost/ 22 | ;; Uncomment (remove the ;;) the line below for self-hosted redis that's not on port 6379 of localhost 23 | ;; :redis-connect-URL "redis://user:pass@host:port/" 24 | } 25 | :landing-page 26 | { 27 | ;; Copy 28 | :page-title "IdeaFerret | Launching soon!" 29 | :app-title "IdeaFerret" 30 | :app-tagline "Find Your Best Idea" 31 | :app-summary "   Uqiquitous Idea Capture
+ Rigorous and Systematic Evaluation
= Your Best Idea" ; OK to be blank 32 | :instructions "Here's an idea! We can notify you when it's ready." 33 | :placeholder "you@email.com" ; OK to be blank 34 | :sign-up-btn "Sign up" 35 | :spam-msg "" ; OK to be blank 36 | :thank-you "Thank you! We'll be in touch soon." 37 | :error "Sorry! Our system is temporarily down, please try again later." 38 | :copyright "" ; OK to be blank 39 | 40 | ;; Fonts 41 | 42 | ;; see font advice at: 43 | ;; http://designshack.net/articles/css/10-great-google-font-combinations-you-can-copy/ 44 | ;; http://designshack.net/articles/typography/10-more-great-google-font-combinations-you-can-copy/ 45 | ;; http://www.insquaremedia.com/blog/15-web-design-stuff/50-our-favourite-google-font-combinations 46 | 47 | ;; see https://www.google.com/fonts for available fonts 48 | :app-title-font "Bree Serif" ; Google web font 49 | :app-title-backup-fonts "Georgia,serif" ; Web-safe fonts 50 | :body-font "Open Sans" ; Google web font 51 | :body-backup-fonts "Arial,Helvetica,sans-serif" ; Web-safe fonts 52 | 53 | ;; Images 54 | :logo "" ; OK to be blank 55 | :background-image "/img/ferret.jpg" ; OK to be blank 56 | 57 | ;; Colors 58 | :background-color "#FFF" ; white 59 | :container-bg-color "#FFF" ; white 60 | :container-opacity "0" ; 1 for solid, 0 for no container, 0.x for some transparency 61 | :app-title-color "#63A687" ; greenish 62 | :app-tagline-color "#000" ; black 63 | :app-summary-color "#000" ; black 64 | :instructions-color "#000" ; black 65 | :spam-msg-color "#646464" ; dark grey 66 | :thank-you-color "#63A687" ; greenish 67 | :error-color "#F00" ; red 68 | :copyright-color "#646464" ; dark grey 69 | :social-color "#646464" ; dark grey 70 | :social-hover-color "#63A687" ; greenish 71 | 72 | ;; Signup button 73 | :signup-btn-icon "fa-lightbulb-o" ; see http://fortawesome.github.io/Font-Awesome/cheatsheet/ OK to be blank 74 | :signup-btn-class "btn-default" ; see http://getbootstrap.com/css/#buttons 75 | 76 | ;; Social stuff 77 | :twitter-url "http://twitter.com/ideaferret" ; OK to be blank 78 | :twitter-title "" ; OK to be blank 79 | :facebook-url "" ; OK to be blank 80 | :facebook-title "" ; OK to be blank 81 | :github-url "" ; OK to be blank 82 | :github-title "" ; OK to be blank 83 | :blog-url "http://blog.snootymonkey.com/" ; OK to be blank 84 | :blog-title "" ; OK to be blank 85 | :blog-feed "http://feeds.feedburner.com/snootymonkey" ; OK to be blank 86 | 87 | ;; Analytics 88 | :analytics false ; include the analytics snippet in src/coming-soon/templates/analytics.html 89 | } 90 | } -------------------------------------------------------------------------------- /src/coming_soon/views/contacts.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.views.contacts 2 | (:require [clojure.string :refer (blank?)] 3 | [net.cgrand.enlive-html :refer :all] 4 | [coming-soon.config :refer (landing-page)] 5 | [coming-soon.lib.colors :refer (rgb-color rgba-color)])) 6 | 7 | (def google-font-url "//fonts.googleapis.com/css?family=") 8 | 9 | (defn- analytics-content [] 10 | (let [analytics (first (html-resource "coming_soon/templates/analytics.html"))] 11 | ;; strip the tags if necessary 12 | (if (= :html (:tag analytics)) 13 | (:content analytics) 14 | analytics))) 15 | 16 | (defn- blog-feed-content [] 17 | (html [:link {:rel "alternate" :type "application/rss+xml" :href (landing-page :blog-feed)}])) 18 | 19 | (defn- linked-icon [{:keys [link-name icon-name]}] 20 | (let [url (landing-page (keyword (str link-name "-url"))) 21 | title (landing-page (keyword (str link-name "-title")))] 22 | (if-not (blank? url) 23 | (html [:li [:a {:href url :title title :class "social-link" :target "_new"} 24 | [:i {:class (str "fa fa-" icon-name " fa-lg")}]]])))) 25 | 26 | (defn- signup-button-icon [] 27 | (let [signup-btn-icon-class (landing-page :signup-btn-icon)] 28 | (if-not (blank? signup-btn-icon-class) 29 | (html [:i {:id "submit-icon" :class (str "fa " signup-btn-icon-class)}] " ")))) 30 | 31 | (defn- background-image [] 32 | (let [image-url (landing-page :background-image)] 33 | (if-not (blank? image-url) 34 | (str "$.backstretch('" image-url "');") 35 | ""))) 36 | 37 | (deftemplate home-page "coming_soon/templates/home.html" [referrer] 38 | ;; head 39 | [:title] (content (landing-page :page-title)) 40 | [:#google-title-font] (set-attr :href (str google-font-url (landing-page :app-title-font))) 41 | [:#google-body-font] (set-attr :href (str google-font-url (landing-page :body-font))) 42 | [:#configured-styles] (html-content (str 43 | "body {background-color:" (landing-page :background-color) ";" 44 | "font-family:" (landing-page :body-font) "," (landing-page :body-backup-fonts) ";}" 45 | "#main-container {background:" (rgb-color (landing-page :container-bg-color)) ";" 46 | "background:" (rgba-color (landing-page :container-bg-color) (landing-page :container-opacity)) ";}" 47 | "img.logo {display:" (if (blank? (landing-page :logo)) "none" "inline") ";}" 48 | "#app-title {color:" (landing-page :app-title-color) ";" 49 | "font-family:" (landing-page :app-title-font) "," (landing-page :app-title-backup-fonts) ";}" 50 | "#app-tagline {color:" (landing-page :app-tagline-color) ";}" 51 | "#app-summary {color:" (landing-page :app-summary-color) ";}" 52 | "#instructions {color:" (landing-page :instructions-color) ";}" 53 | "#thank-you {color:" (landing-page :thank-you-color) ";}" 54 | "#error-message {color:" (landing-page :error-color) ";}" 55 | "#spam-msg {color:" (landing-page :spam-msg-color) ";}" 56 | "a.social-link {color:" (landing-page :social-color) ";}" 57 | "a.social-link:hover {color:" (landing-page :social-hover-color) ";}" 58 | "#copyright {color:" (landing-page :copyright-color) ";}")) 59 | [:head] (append 60 | (if-not (blank? (landing-page :blog-feed)) 61 | (blog-feed-content) 62 | "")) 63 | 64 | ;; body 65 | [:#backstretch] (html-content (background-image)) 66 | 67 | ;; container 68 | [:img.logo] (set-attr :src (landing-page :logo)) 69 | [:#app-title] (html-content (landing-page :app-title)) 70 | [:#app-tagline] (html-content (landing-page :app-tagline)) 71 | [:#app-summary] (html-content (landing-page :app-summary)) 72 | [:#instructions] (html-content (landing-page :instructions)) 73 | [:#thank-you] (html-content (landing-page :thank-you)) 74 | [:#error-message] (html-content (landing-page :error)) 75 | [:#email] (set-attr :placeholder (landing-page :placeholder)) 76 | [:#referrer] (set-attr :value referrer) 77 | [:#submit] (add-class (landing-page :signup-btn-class)) 78 | [:#submit] (content (signup-button-icon) (landing-page :sign-up-btn)) 79 | [:#spam-msg] (html-content (landing-page :spam-msg)) 80 | [:#social-links] (content (map linked-icon [ 81 | {:link-name "twitter" :icon-name "twitter"} 82 | {:link-name "facebook" :icon-name "facebook"} 83 | {:link-name "github" :icon-name "github"} 84 | {:link-name "blog" :icon-name "rss"}])) 85 | 86 | ;; footer 87 | [:#copyright] (html-content (landing-page :copyright)) 88 | 89 | ;; scripts 90 | [:body] (append (if (landing-page :analytics) (analytics-content) ""))) -------------------------------------------------------------------------------- /examples/silver_bullet/config.edn: -------------------------------------------------------------------------------- 1 | { 2 | :coming-soon 3 | { 4 | :admin-user "admin" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_USER 5 | :admin-password "change_me!" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_PASSWORD 6 | :instance-prefix "coming-soon" ; make this unique if you are sharing a single redis instance w/ another coming-soon instance 7 | } 8 | :redis 9 | { 10 | ;; If you're running Redis locally on the same machine and the default 6379 port, leave everything in this section commented out 11 | 12 | ;; HEROKU REDIS - uncomment the selected provider (remove the ;;) 13 | ;; :redis-env-variable "REDISCLOUD_URL" ; Comment in for Redis Cloud on Heroku 14 | ;; :redis-env-variable "REDISTOGO_URL" ; Comment in for Redis To Go on Heroku 15 | ;; :redis-env-variable "OPENREDIS_URL" ; Comment in for openredis on Heroku 16 | ;; :redis-env-variable "REDISGREEN_URL" ; Comment in for RedisGreen on Heroku 17 | 18 | ;; SELF HOSTED REDIS 19 | ;; A different port on the local machine might be: redis://localhost:7000/ 20 | ;; The default port, but on a different server might be: redis://10.1.0.70/ 21 | ;; Local on the default port, but with authentication might be: redis://admin:supersecret@localhost/ 22 | ;; Uncomment (remove the ;;) the line below for self-hosted redis that's not on port 6379 of localhost 23 | ;; :redis-connect-URL "redis://user:pass@host:port/" 24 | } 25 | :landing-page 26 | { 27 | ;; Copy 28 | :page-title "Silver Bullet | Launching soon!" 29 | :app-title "Silver Bullet" 30 | :app-tagline "Clojure powered bad-assery!" 31 | :app-summary "With Silver Bullet, all your problems are gone.
Silver Bullet smacks your problems around a bit, then sends them packing.
You want it. You need it." ; OK to be blank 32 | :instructions "We'll notify you when it's ready." 33 | :placeholder "Email Address*" ; OK to be blank 34 | :sign-up-btn "Sign up" 35 | :spam-msg "* We won't spam you. We hate spam as much as you do." ; OK to be blank 36 | :thank-you "Thank you! We'll be in touch soon." 37 | :error "Sorry! Our system is temporarily down, please try again later." 38 | :copyright "© 2017 Clojurific Studios" ; OK to be blank 39 | 40 | ;; Fonts 41 | 42 | ;; see font advice at: 43 | ;; http://designshack.net/articles/css/10-great-google-font-combinations-you-can-copy/ 44 | ;; http://designshack.net/articles/typography/10-more-great-google-font-combinations-you-can-copy/ 45 | ;; http://www.insquaremedia.com/blog/15-web-design-stuff/50-our-favourite-google-font-combinations 46 | 47 | ;; see https://www.google.com/fonts for available fonts 48 | :app-title-font "Droid Sans" ; Google web font 49 | :app-title-backup-fonts "Arial,Helvetica,sans-serif" ; Web-safe fonts 50 | :body-font "Droid Serif" ; Google web font 51 | :body-backup-fonts "Georgia,serif" ; Web-safe fonts 52 | 53 | ;; Images 54 | :logo "/img/logo.png" ; OK to be blank 55 | :background-image "" ; OK to be blank 56 | 57 | ;; Colors 58 | :background-color "#403727" ; brown 59 | :container-bg-color "#EEE" ; light grey 60 | :container-opacity "1" ; 1 for solid, 0 for no container, 0.x for some transparency 61 | :app-title-color "#63A687" ; greenish 62 | :app-tagline-color "#000" ; black 63 | :app-summary-color "#000" ; black 64 | :instructions-color "#000" ; black 65 | :spam-msg-color "#646464" ; dark grey 66 | :thank-you-color "#63A687" ; greenish 67 | :error-color "#F00" ; red 68 | :copyright-color "#646464" ; dark grey 69 | :social-color "#646464" ; dark grey 70 | :social-hover-color "#63A687" ; greenish 71 | 72 | ;; Signup button 73 | :signup-btn-icon "fa-envelope-o" ; see http://fortawesome.github.io/Font-Awesome/cheatsheet/ OK to be blank 74 | :signup-btn-class "btn-success" ; see http://getbootstrap.com/css/#buttons 75 | 76 | ;; Social stuff 77 | :twitter-url "http://twitter.com/snootymonkey" ; OK to be blank 78 | :twitter-title "Follow Snooty Monkey on Twitter" ; OK to be blank 79 | :facebook-url "https://www.facebook.com/bubbletimer" ; OK to be blank 80 | :facebook-title "BubbleTimer is on Facebook" ; OK to be blank 81 | :github-url "https://github.com/SnootyMonkey" ; OK to be blank 82 | :github-title "Snooty Monkey's open source GitHub repositiories" ; OK to be blank 83 | :blog-url "http://blog.snootymonkey.com/" ; OK to be blank 84 | :blog-title "Subscribe to Monkey Opus, the Snooty Monkey blog" ; OK to be blank 85 | :blog-feed "http://feeds.feedburner.com/snootymonkey" ; OK to be blank 86 | 87 | ;; Analytics 88 | :analytics false ; include the analytics snippet in src/coming-soon/templates/analytics.html 89 | } 90 | } -------------------------------------------------------------------------------- /src/coming_soon/models/contact.clj: -------------------------------------------------------------------------------- 1 | (ns coming-soon.models.contact 2 | (:require [taoensso.carmine :as car] 3 | [coming-soon.lib.redis :refer (prefix with-car)] 4 | [coming-soon.models.email :refer (valid?)] 5 | [clj-time.format :refer (formatters unparse)] 6 | [clj-time.core :refer (now)])) 7 | 8 | ;; Redis "schema" 9 | ;; :coming-soon-id - a simple incrementing counter 10 | (def coming-soon-id (str prefix ":coming-soon-id")) 11 | ;; :coming-soon-contacts - hash of all contacts by id 12 | (def coming-soon-contacts (str prefix ":coming-soon-contacts")) 13 | ;; :coming-soon-emails - hash of all ids, hashed by email (a secondary index) 14 | (def coming-soon-emails (str prefix ":coming-soon-emails")) 15 | 16 | (def timestamp-format (formatters :date-time-no-ms)) 17 | 18 | (defn exists-by-email? 19 | "Determine if a contact with the specified email address has been collected." 20 | [email] 21 | (= 1 (with-car (car/hexists coming-soon-emails email)))) 22 | 23 | (defn exists-by-id? 24 | "Determine if a contact with the specified id has been collected." 25 | [id] 26 | (= 1 (with-car (car/hexists coming-soon-contacts id)))) 27 | 28 | (defn contact-by-id 29 | "Return the contact for an id, nil if no contact exists by that id." 30 | [id] 31 | (when (exists-by-id? id) 32 | (read-string (with-car (car/hget coming-soon-contacts id))))) 33 | 34 | (defn contact-by-email 35 | "Return the contact for an email, nil if the email has not been collected." 36 | [email] 37 | (when (exists-by-email? email) 38 | (let [id (with-car (car/hget coming-soon-emails email))] 39 | (contact-by-id id)))) 40 | 41 | (defn contact-count 42 | "Return the number of contacts collected." 43 | [] 44 | (with-car (car/hlen coming-soon-contacts))) 45 | 46 | (defn all-contacts 47 | "Return all the contacts that have been collected so far in date of most recent collection order." 48 | [] 49 | (reverse (sort-by :id (map read-string (with-car (car/hvals coming-soon-contacts)))))) 50 | 51 | (defn all-emails 52 | "Return all the emails that have been collected so far in date of collection order." 53 | [] 54 | (map :email (all-contacts))) 55 | 56 | (defn all-referrers 57 | "Return all the referrer URLs and the # of referrals by that URL, in order of most referrals." 58 | [] 59 | (sort-by last > (map identity (frequencies (remove nil? (map :referrer (all-contacts))))))) 60 | 61 | ;; ISO 8601 timestamp 62 | (defn- current-timestamp [] 63 | (unparse timestamp-format (now))) 64 | 65 | ;; Store the contact by email and by id 66 | (defn- store [id email referrer] 67 | (with-car 68 | ;; start a transaction 69 | (car/multi) 70 | ;; hash the id by the email 71 | (car/hset coming-soon-emails email id) 72 | ;; hash the contact by the id 73 | (car/hset coming-soon-contacts id (pr-str 74 | {:id id 75 | :email email 76 | :referrer referrer 77 | :updated-at (current-timestamp)})) 78 | ;; commit the transaction 79 | (car/exec))) 80 | 81 | (defn create 82 | "Store the contact if the email is valid, otherwise return :invalid-email" 83 | [email referrer] 84 | (if (valid? email) 85 | ; existing id or new id 86 | (let [id (if (exists-by-email? email) 87 | (:id (contact-by-email email)) 88 | (with-car (car/incr coming-soon-id)))] 89 | (store id email referrer) 90 | true) 91 | :invalid-email)) 92 | 93 | ;; Remove the contact from the hash by id and the hash by email 94 | (defn- remove! [id email] 95 | (with-car 96 | ;; start a transaction 97 | (car/multi) 98 | ;; remove the contact 99 | (car/hdel coming-soon-contacts id) 100 | (car/hdel coming-soon-emails email) 101 | ;; commit the transaction 102 | (car/exec))) 103 | 104 | (defn remove-by-email! 105 | "Remove the contact." 106 | [email] 107 | (if (exists-by-email? email) 108 | (do 109 | (let [id (with-car (car/hget coming-soon-emails email))] 110 | (remove! id email)) 111 | true) 112 | false)) 113 | 114 | (defn remove-by-id! 115 | "Remove the contact." 116 | [id] 117 | (if (exists-by-id? id) 118 | (do 119 | (let [email (:email (contact-by-id id))] 120 | (remove! id email)) 121 | true) 122 | false)) 123 | 124 | (defn erase! 125 | "USE WITH CAUTION - Wipes out all the stored contacts." 126 | [] 127 | (with-car 128 | ;; start a transaction 129 | (car/multi) 130 | ;; remove the hashes and counter 131 | (car/del coming-soon-id) 132 | (car/del coming-soon-contacts) 133 | (car/del coming-soon-emails) 134 | ;; commit the transaction 135 | (car/exec)) 136 | true) 137 | 138 | (defn sane? 139 | "Is the contact store in a sane state?" 140 | [] 141 | (= (with-car (car/hlen coming-soon-contacts)) (with-car (car/hlen coming-soon-emails)))) 142 | 143 | (defn test-populate 144 | "Create n many test registrations for whatever reason you may have for doing so" 145 | [n] 146 | (when (pos? n) 147 | (create (str "test.user" n "@testing.com") (str "http://testing" n ".com/article/test.html")) 148 | (recur (dec n)))) -------------------------------------------------------------------------------- /config.edn: -------------------------------------------------------------------------------- 1 | { 2 | :coming-soon { 3 | :admin-user "admin" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_USER 4 | :admin-password "change_me!" ; CHANGE THIS! Only use if config.edn is private! Otherwise use ENV var: ADMIN_PASSWORD 5 | :instance-prefix "coming-soon" ; make this unique if you are sharing a single redis instance w/ another coming-soon instance 6 | } 7 | :redis { 8 | ;; If you're running Redis locally on the same machine and the default 6379 port, leave everything in this section commented out 9 | 10 | ;; HEROKU REDIS - uncomment the selected provider (remove the ;;) 11 | ;; :redis-env-variable "REDISCLOUD_URL" ; Comment in for Redis Cloud on Heroku 12 | ;; :redis-env-variable "REDISTOGO_URL" ; Comment in for Redis To Go on Heroku 13 | ;; :redis-env-variable "OPENREDIS_URL" ; Comment in for openredis on Heroku 14 | ;; :redis-env-variable "REDISGREEN_URL" ; Comment in for RedisGreen on Heroku 15 | 16 | ;; SELF HOSTED REDIS 17 | ;; A different port on the local machine might be: redis://localhost:7000/ 18 | ;; The default port, but on a different server might be: redis://10.1.0.70/ 19 | ;; Local on the default port, but with authentication might be: redis://admin:supersecret@localhost/ 20 | ;; Uncomment (remove the ;;) the line below for self-hosted redis that's not on port 6379 of localhost 21 | ;; :redis-connect-URL "redis://user:pass@host:port/" 22 | } 23 | :webhooks { 24 | ;; :posthere-io { 25 | ;; ;; POSThere.io is a webhook callback testing service you can use to test webhooks 26 | ;; :handler "coming-soon.webhooks.posthere-io" 27 | ;; 28 | ;; ;; change me to something unique for you 29 | ;; :posthere-io-uuid "coming-soon-test" 30 | ;; } 31 | ;; :mailchimp { 32 | ;; :handler "coming-soon.webhooks.mailchimp" 33 | ;; :api-key "" ; only set this if your config.edn is private! Otherwise use the ENV variable: MAILCHIMP_API_KEY 34 | ;; :list-id "" ; REQUIRED! ID of the MailChimp list to add new contacts to (displayed in the list's settings) 35 | ;; :status "pending" ; Mailchimp status of the new subscriber ('pending' or 'subscribed', 'pending' gets an email confirmation) 36 | ;; } 37 | ;; :slack { 38 | ;; :handler "coming-soon.webhooks.slack" 39 | ;; :api-key "" ; only set this if your config.edn is private! Otherwise use the ENV variable: SLACK_API_KEY 40 | ;; :channel-name "" 41 | ;; } 42 | } 43 | :landing-page { 44 | ;; Copy 45 | :page-title "Silver Bullet | Launching soon!" 46 | :app-title "Silver Bullet" 47 | :app-tagline "Clojure powered bad-assery!" 48 | :app-summary "With Silver Bullet, all your problems are gone.
Silver Bullet smacks your problems around a bit, then sends them packing.
You want it. You need it." ; OK to be blank 49 | :instructions "We'll notify you when it's ready." 50 | :placeholder "Email Address*" ; OK to be blank 51 | :sign-up-btn "Sign up" 52 | :spam-msg "* We won't spam you. We hate spam as much as you do." ; OK to be blank 53 | :thank-you "Thank you! We'll be in touch soon." 54 | :error "Sorry! Our system is temporarily down, please try again later." 55 | :copyright "© 2017 Clojurific Studios" ; OK to be blank 56 | 57 | ;; Fonts 58 | 59 | ;; see font advice at: 60 | ;; http://designshack.net/articles/css/10-great-google-font-combinations-you-can-copy/ 61 | ;; http://designshack.net/articles/typography/10-more-great-google-font-combinations-you-can-copy/ 62 | ;; http://www.insquaremedia.com/blog/15-web-design-stuff/50-our-favourite-google-font-combinations 63 | 64 | ;; see https://www.google.com/fonts for available fonts 65 | :app-title-font "Droid Sans" ; Google web font 66 | :app-title-backup-fonts "Arial,Helvetica,sans-serif" ; Web-safe fonts 67 | :body-font "Droid Serif" ; Google web font 68 | :body-backup-fonts "Georgia,serif" ; Web-safe fonts 69 | 70 | ;; Images 71 | :logo "/img/logo.png" ; OK to be blank 72 | :background-image "" ; OK to be blank 73 | 74 | ;; Colors 75 | :background-color "#403727" ; brown 76 | :container-bg-color "#EEE" ; light grey 77 | :container-opacity "1" ; 1 for solid, 0 for no container, 0.x for some transparency 78 | :app-title-color "#63A687" ; greenish 79 | :app-tagline-color "#000" ; black 80 | :app-summary-color "#000" ; black 81 | :instructions-color "#000" ; black 82 | :spam-msg-color "#646464" ; dark grey 83 | :thank-you-color "#63A687" ; greenish 84 | :error-color "#F00" ; red 85 | :copyright-color "#646464" ; dark grey 86 | :social-color "#646464" ; dark grey 87 | :social-hover-color "#63A687" ; greenish 88 | 89 | ;; Signup button 90 | :signup-btn-icon "fa-envelope-o" ; see http://fortawesome.github.io/Font-Awesome/cheatsheet/ OK to be blank 91 | :signup-btn-class "btn-success" ; see http://getbootstrap.com/css/#buttons 92 | 93 | ;; Social stuff 94 | :twitter-url "http://twitter.com/snootymonkey" ; OK to be blank 95 | :twitter-title "Follow Snooty Monkey on Twitter" ; OK to be blank 96 | :facebook-url "https://www.facebook.com/bubbletimer" ; OK to be blank 97 | :facebook-title "BubbleTimer is on Facebook" ; OK to be blank 98 | :github-url "https://github.com/SnootyMonkey" ; OK to be blank 99 | :github-title "Snooty Monkey's open source GitHub repositiories" ; OK to be blank 100 | :blog-url "http://blog.snootymonkey.com/" ; OK to be blank 101 | :blog-title "Subscribe to Monkey Opus, the Snooty Monkey blog" ; OK to be blank 102 | :blog-feed "http://feeds.feedburner.com/snootymonkey" ; OK to be blank 103 | 104 | ;; Analytics 105 | :analytics false ; include the analytics snippet in src/coming-soon/templates/analytics.html 106 | } 107 | } -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject coming-soon "0.3.0-SNAPSHOT" 2 | :description "coming-soon is a simple Clojure/ClojureScript/Redis 'landing page' application that takes just a few minute to setup" 3 | :url "https://github.com/SnootyMonkey/coming-soon/" 4 | :license {:name "Mozilla Public License v2.0" 5 | :url "http://www.mozilla.org/MPL/2.0/"} 6 | 7 | :min-lein-version "2.7.1" 8 | 9 | :dependencies [ 10 | ;; Server-side 11 | [org.clojure/clojure "1.9.0-alpha17"] ; Lisp on the JVM http://clojure.org/documentation 12 | [ring/ring-jetty-adapter "1.6.2"] ; Web Server https://github.com/ring-clojure/ring 13 | [ring-basic-authentication "1.0.5"] ; Basic HTTP/S Auth https://github.com/remvee/ring-basic-authentication 14 | [compojure "1.6.0"] ; Web routing http://github.com/weavejester/compojure 15 | [com.taoensso/carmine "2.16.0"] ; Redis client https://github.com/ptaoussanis/carmine 16 | [org.clojure/core.async "0.3.443"] ; Async programming library https://github.com/clojure/core.async/ 17 | [environ "1.1.0"] ; Get environment settings from different sources https://github.com/weavejester/environ 18 | [clj-http "3.6.1"] ; HTTP client https://github.com/dakrone/clj-http 19 | [clj-json "0.5.3"] ; JSON encoding https://github.com/mmcgrana/clj-json/ 20 | [org.clojure/data.xml "0.2.0-alpha2"] ; XML encoding https://github.com/clojure/data.xml 21 | [clojure-csv/clojure-csv "2.0.2"] ; CSV encoding https://github.com/davidsantiago/clojure-csv 22 | [enlive "1.1.6"] ; HTML templates https://github.com/cgrand/enlive 23 | [hiccup "2.0.0-alpha1"] ; HTML generation https://github.com/weavejester/hiccup 24 | [tinter "0.1.1-SNAPSHOT"] ; color manipulation https://github.com/andypayne/tinter 25 | [clj-time "0.14.0"] ; DateTime utilities https://github.com/clj-time/clj-time 26 | ;; Client-side 27 | [org.clojure/clojurescript "1.9.671"] ; ClojureScript compiler https://github.com/clojure/clojurescript 28 | [jayq "2.5.4"] ; ClojureScript wrapper for jQuery https://github.com/ibdknox/jayq 29 | ] 30 | 31 | :plugins [ 32 | [lein-ring "0.12.0"] ; Common ring tasks https://github.com/weavejester/lein-ring 33 | [lein-environ "1.1.0"] ; Get environment settings from different sources https://github.com/weavejester/environ 34 | ] 35 | 36 | :profiles { 37 | :qa { 38 | :dependencies [ 39 | [midje "1.9.0-alpha8"] ; Example-based testing https://github.com/marick/Midje 40 | [ring-mock "0.1.5"] ; Test Ring requests https://github.com/weavejester/ring-mock 41 | ] 42 | :plugins [ 43 | [lein-midje "3.2.1"] ; Example-based testing https://github.com/marick/lein-midje 44 | [jonase/eastwood "0.2.4"] ; Clojure linter https://github.com/jonase/eastwood 45 | [lein-kibit "0.1.6-beta1"] ; Static code search for non-idiomatic code https://github.com/jonase/kibit 46 | ] 47 | :env { 48 | :config-file "test/test-config.edn" 49 | } 50 | :cucumber-feature-paths ["test/coming_soon/features"] 51 | } 52 | :dev [:qa { 53 | :plugins [ 54 | [lein-ancient "0.6.10"] ; Check for outdated dependencies https://github.com/xsc/lein-ancient 55 | [lein-cljsbuild "1.1.6"] ; ClojureScript compiler https://github.com/emezeske/lein-cljsbuild 56 | [lein-spell "0.1.0"] ; Catch spelling mistakes in docs and docstrings https://github.com/cldwalker/lein-spell 57 | [lein-bikeshed "0.4.1"] ; Check for code smells https://github.com/dakrone/lein-bikeshed 58 | [lein-checkall "0.1.1"] ; Runs bikeshed, kibit and eastwood https://github.com/itang/lein-checkall 59 | [lein-cljfmt "0.5.6"] ; Code formatting https://github.com/weavejester/cljfmt 60 | [lein-deps-tree "0.1.2"] ; Print a tree of project dependencies https://github.com/the-kenny/lein-deps-tree 61 | [venantius/yagni "0.1.4"] ; Dead code finder https://github.com/venantius/yagni 62 | ] 63 | :env { 64 | :config-file "config.edn" 65 | } 66 | :cljfmt { 67 | :file-pattern #"\/src\/.+\.clj[csx]?$" 68 | } 69 | }] 70 | :repl [:dev { 71 | :dependencies [ 72 | [org.clojure/tools.nrepl "0.2.13"] ; Network REPL https://github.com/clojure/tools.nrepl 73 | [aprint "0.1.3"] ; Pretty printing in the REPL (aprint ...) https://github.com/razum2um/aprint 74 | ] 75 | ;; REPL injections 76 | :injections [ 77 | (require '[aprint.core :refer (aprint ap)] 78 | '[clojure.stacktrace :refer (print-stack-trace)] 79 | '[clojure.test :refer :all] 80 | '[clj-time.format :as t] 81 | '[clojure.string :as s]) 82 | ] 83 | }] 84 | :prod { 85 | :env { 86 | :config-file "config.edn" 87 | } 88 | } 89 | } 90 | 91 | :aliases { 92 | "build" ["with-profile" "prod" "do" "clean," "deps," ["cljsbuild" "once"] "uberjar"] 93 | "midje!" ["with-profile" "qa" "midje"] 94 | "test!" ["with-profile" "qa" "midje"] 95 | "autotest" ["with-profile" "qa" "midje" ":autotest"] ; watch for code changes and run affected tests 96 | "test-server" ["with-profile" "qa" "ring" "server-headless"] 97 | "start" ["with-profile" "dev" "ring" "server-headless"] 98 | "start!" ["ring" "server-headless"] 99 | "repl" ["with-profile" "repl" "repl"] 100 | "spell" ["spell" "-n"] 101 | "bikeshed!" ["bikeshed" "-v" "-m" "120"] ; code check with max line length warning of 120 characters 102 | "ancient" ["ancient" ":all" ":allow-qualified"] ; check for out of date dependencies 103 | } 104 | 105 | ;; ----- Code check configuration ----- 106 | 107 | :eastwood { 108 | ;; Dinable some linters that are enabled by default 109 | :exclude-linters [:wrong-arity] 110 | ;; Enable some linters that are disabled by default 111 | :add-linters [:unused-private-vars :unused-locals] ; :unused-namespaces (ideal, but doesn't see in macros) 112 | 113 | ;; Exclude testing namespaces 114 | :tests-paths ["test"] 115 | :exclude-namespaces [:test-paths] 116 | } 117 | 118 | ;; ----- ClojureScript ----- 119 | 120 | :cljsbuild { 121 | :builds 122 | [{ 123 | :source-paths ["src/coming_soon/cljs" "src"] ; CLJS source code path 124 | ;; Google Closure (CLS) options configuration 125 | :compiler { 126 | :output-to "resources/public/js/coming_soon.js" ; generated JS script filename 127 | :optimizations :simple ; JS optimization directive 128 | :pretty-print false ; generated JS code prettyfication 129 | }}] 130 | } 131 | 132 | ;; ----- Web Application ----- 133 | 134 | :ring {:handler coming-soon.app/app} 135 | :main coming-soon.app 136 | :aot [coming-soon.app] 137 | ) -------------------------------------------------------------------------------- /test/coming_soon/features/step_definitions/models/contact_steps.clj.old: -------------------------------------------------------------------------------- 1 | ; ;; Behavioral driven unit tests for the contact model 2 | ; (require '[coming-soon.lib.check :refer (check)] 3 | ; '[coming-soon.models.contact :as contact] 4 | ; '[clj-time.core :refer (now before? after? ago seconds)] 5 | ; '[clj-time.format :refer (parse)]) 6 | 7 | ; (def prior-updated-at (atom nil)) 8 | ; (def updated-at (atom nil)) 9 | ; (def result-list (atom nil)) 10 | 11 | ; (defn- add-contact [email referrer] 12 | ; (check (contact/create email referrer)) 13 | ; (reset! prior-updated-at @updated-at) 14 | ; (reset! updated-at (parse (:updated-at (contact/contact-by-email email))))) 15 | 16 | ; (defn- id-for-contact [email] 17 | ; (:id (contact/contact-by-email email))) 18 | 19 | ; (defn- next-result [] 20 | ; (let [next (first @result-list)] 21 | ; (swap! result-list rest) 22 | ; next)) 23 | 24 | ; (Given #"^there are no contacts$" [] 25 | ; (contact/erase!)) 26 | 27 | ; (Then #"^(now the|the) contact count is (\d+)$" [_ contact-count] 28 | ; (check (= (read-string contact-count) (contact/contact-count)))) 29 | 30 | ; (Then #"^the content store is sane$" [] 31 | ; (check (contact/sane?))) 32 | 33 | ; (When #"^I add a contact for \"([^\"]*)\"$" [email] 34 | ; (add-contact email nil)) 35 | 36 | ; (When #"^I add a contact for \"([^\"]*)\" with a referrer from \"([^\"]*)\"$" [email referrer] 37 | ; (add-contact email referrer)) 38 | 39 | ; (Then #"^the contact \"([^\"]*)\" exists$" [email] 40 | ; (check (contact/exists-by-email? email))) 41 | 42 | ; (Then #"^the contact \"([^\"]*)\" does not exist$" [email] 43 | ; (check (not (contact/exists-by-email? email)))) 44 | 45 | ; (Then #"^the contact can be retrieved by \"([^\"]*)\"$" [email] 46 | ; (check 47 | ; (when-let [contact (contact/contact-by-email email)] 48 | ; (= email (:email contact))))) 49 | 50 | ; (Then #"^the contact \"([^\"]*)\" has a referrer of \"([^\"]*)\"$" [email referrer] 51 | ; (check 52 | ; (when-let [contact (contact/contact-by-email email)] 53 | ; (= email (:email contact)) (= referrer (:referrer contact))))) 54 | 55 | ; (Then #"^the contact \"([^\"]*)\" has no referrer$" [email] 56 | ; (check 57 | ; (when-let [contact (contact/contact-by-email email)] 58 | ; (nil? (:referrer contact))))) 59 | 60 | ; (Then #"^the contact \"([^\"]*)\" and the contact \"([^\"]*)\" have unique ids$" [email1 email2] 61 | ; (check (not= (id-for-contact email1) (id-for-contact email2)))) 62 | 63 | ; (Then #"^the contact \"([^\"]*)\" has an id before the contact \"([^\"]*)\"$" [email1 email2] 64 | ; (check (< (id-for-contact email1) (id-for-contact email2)))) 65 | 66 | ; (When #"^I delay a moment$" [] 67 | ; (Thread/sleep 1000)) 68 | 69 | ; (Then #"^the contact \"([^\"]*)\" has an updated-at of just before now$" [email] 70 | ; (check 71 | ; (when-let [contact (contact/contact-by-email email)] 72 | ; (when-let [time (parse (:updated-at contact))] 73 | ; (and (after? time (-> 10 seconds ago)) (before? time (now))))))) 74 | 75 | ; (Then #"^the contact \"([^\"]*)\" has an updated-at before the updated-at for contact \"([^\"]*)\"$" [email1 email2] 76 | ; (check 77 | ; (let [contact1 (contact/contact-by-email email1) 78 | ; contact2 (contact/contact-by-email email2)] 79 | ; (when (and contact1 contact2) 80 | ; (let [time1 (parse (:updated-at contact1)) 81 | ; time2 (parse (:updated-at contact2))] 82 | ; (when (and time1 time2) 83 | ; (before? time1 time2))))))) 84 | 85 | ; (Then #"^the contact \"([^\"]*)\" has a more recent updated-at than before$" [email] 86 | ; (check (before? @prior-updated-at @updated-at))) 87 | 88 | ; (Given #"^the system knows about the following contacts:$" [table] 89 | ; (contact/erase!) 90 | ; (let [users (table->rows table)] 91 | ; (doseq [user users] (contact/create (:email user) (:referrer user))))) 92 | 93 | ; (When #"^I remove the contact for \"([^\"]*)\"$" [email] 94 | ; (contact/remove-by-email! email)) 95 | 96 | ; (When #"^I remove the contact with id (\d+)$" [id] 97 | ; (contact/remove-by-id! id)) 98 | 99 | ; (When #"^I erase all contacts$" [] 100 | ; (contact/erase!)) 101 | 102 | ; (When #"^I retrieve the contact for \"([^\"]*)\" the \"([^\"]*)\" is \"([^\"]*)\"$" [email property value] 103 | ; (let [compare (if (= property "id") (read-string value) value)] 104 | ; (check (= compare ((contact/contact-by-email email) (keyword property)))))) 105 | 106 | ; (When #"^I retrieve the contact for \"([^\"]*)\" the \"([^\"]*)\" is blank$" [email property] 107 | ; (check (nil? ((contact/contact-by-email email) (keyword property))))) 108 | 109 | ; (When #"^I retrieve the contact for id (\d+) the \"([^\"]*)\" is \"([^\"]*)\"$" [id property value] 110 | ; (check (= value ((contact/contact-by-id id) (keyword property))))) 111 | 112 | ; (When #"^I retrieve the contact for id (\d+) the \"([^\"]*)\" is blank$" [id property] 113 | ; (check (nil? ((contact/contact-by-id id) (keyword property))))) 114 | 115 | ; (When #"^I retrieve the contact for \"([^\"]*)\" it doesn't exist$" [email] 116 | ; (check (nil? (contact/contact-by-email email)))) 117 | 118 | ; (When #"^I retrieve the contact for id (\d+) it doesn't exist$" [id] 119 | ; (check (nil? (contact/contact-by-id id)))) 120 | 121 | ; (When #"^I list all contacts$" [] 122 | ; (reset! result-list (contact/all-contacts))) 123 | 124 | ; (When #"^I list all emails$" [] 125 | ; (reset! result-list (contact/all-emails))) 126 | 127 | ; (When #"^I list all referrals$" [] 128 | ; (reset! result-list (contact/all-referrers))) 129 | 130 | ; (Then #"^the list contains (\d+) items$" [item-count] 131 | ; (check (= (read-string item-count) (count @result-list)))) 132 | 133 | ; (Then #"^the next contact is \"([^\"]*)\" with the id (\d+) and the referrer \"([^\"]*)\"$" [email id referrer] 134 | ; (let [contact (next-result)] 135 | ; (check (= email (:email contact))) 136 | ; (check (= (read-string id) (:id contact))) 137 | ; (check (= referrer (:referrer contact))))) 138 | 139 | ; (Then #"^the next contact is \"([^\"]*)\" with the id (\d+) and no referrer$" [email id] 140 | ; (let [contact (next-result)] 141 | ; (check (= email (:email contact))) 142 | ; (check (= (read-string id) (:id contact))) 143 | ; (check (nil? (:referrer contact))))) 144 | 145 | ; (Then #"^the next email is \"([^\"]*)\"$" [expected-email] 146 | ; (let [actual-email (next-result)] 147 | ; (check (= expected-email actual-email)))) 148 | 149 | ; (Then #"^the next referrer is \"([^\"]*)\" with a count of (\d+)$" [url referral-count] 150 | ; (let [referrer (next-result)] 151 | ; (check (= url (first referrer))) 152 | ; (check (= (read-string referral-count) (last referrer))))) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2013-2017 Snooty Monkey, LLC 2 | http://snootymonkey.com/ 3 | 4 | Mozilla Public License 5 | Version 2.0 6 | 7 | 1. Definitions 8 | 9 | 1.1. “Contributor” 10 | means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 11 | 12 | 1.2. “Contributor Version” 13 | means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor’s Contribution. 14 | 15 | 1.3. “Contribution” 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. “Covered Software” 19 | means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 20 | 21 | 1.5. “Incompatible With Secondary Licenses” 22 | means 23 | 24 | that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or 25 | 26 | that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 27 | 28 | 1.6. “Executable Form” 29 | means any form of the work other than Source Code Form. 30 | 31 | 1.7. “Larger Work” 32 | means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 33 | 34 | 1.8. “License” 35 | means this document. 36 | 37 | 1.9. “Licensable” 38 | means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 39 | 40 | 1.10. “Modifications” 41 | means any of the following: 42 | 43 | any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or 44 | 45 | any new file in Source Code Form that contains any Covered Software. 46 | 47 | 1.11. “Patent Claims” of a Contributor 48 | means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 49 | 50 | 1.12. “Secondary License” 51 | means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 52 | 53 | 1.13. “Source Code Form” 54 | means the form of the work preferred for making modifications. 55 | 56 | 1.14. “You” (or “Your”) 57 | means an individual or a legal entity exercising rights under this License. For legal entities, “You” includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 58 | 59 | 2. License Grants and Conditions 60 | 61 | 2.1. Grants 62 | 63 | Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: 64 | 65 | under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and 66 | 67 | under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 68 | 69 | 2.2. Effective Date 70 | 71 | The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 72 | 73 | 2.3. Limitations on Grant Scope 74 | 75 | The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: 76 | 77 | for any code that a Contributor has removed from Covered Software; or 78 | 79 | for infringements caused by: (i) Your and any other third party’s modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or 80 | 81 | under Patent Claims infringed by Covered Software in the absence of its Contributions. 82 | 83 | This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 84 | 85 | 2.4. Subsequent Licenses 86 | 87 | No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 88 | 89 | 2.5. Representation 90 | 91 | Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 92 | 93 | 2.6. Fair Use 94 | 95 | This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 96 | 97 | 2.7. Conditions 98 | 99 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 100 | 101 | 3. Responsibilities 102 | 103 | 3.1. Distribution of Source Form 104 | 105 | All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients’ rights in the Source Code Form. 106 | 107 | 3.2. Distribution of Executable Form 108 | 109 | If You distribute Covered Software in Executable Form then: 110 | 111 | such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and 112 | 113 | You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients’ rights in the Source Code Form under this License. 114 | 115 | 3.3. Distribution of a Larger Work 116 | 117 | You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 118 | 119 | 3.4. Notices 120 | 121 | You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 122 | 123 | 3.5. Application of Additional Terms 124 | 125 | You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 126 | 127 | 4. Inability to Comply Due to Statute or Regulation 128 | 129 | If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 130 | 131 | 5. Termination 132 | 133 | 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 134 | 135 | 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 136 | 137 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. 138 | 139 | 6. Disclaimer of Warranty 140 | 141 | Covered Software is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the Covered Software is free of defects, merchantable, fit for a particular purpose or non-infringing. The entire risk as to the quality and performance of the Covered Software is with You. Should any Covered Software prove defective in any respect, You (not any Contributor) assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty constitutes an essential part of this License. No use of any Covered Software is authorized under this License except under this disclaimer. 142 | 143 | 7. Limitation of Liability 144 | 145 | Under no circumstances and under no legal theory, whether tort (including negligence), contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as permitted above, be liable to You for any direct, indirect, special, incidental, or consequential damages of any character including, without limitation, damages for lost profits, loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses, even if such party shall have been informed of the possibility of such damages. This limitation of liability shall not apply to liability for death or personal injury resulting from such party’s negligence to the extent applicable law prohibits such limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this exclusion and limitation may not apply to You. 146 | 147 | 8. Litigation 148 | 149 | Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party’s ability to bring cross-claims or counter-claims. 150 | 151 | 9. Miscellaneous 152 | 153 | This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 154 | 155 | 10. Versions of the License 156 | 157 | 10.1. New Versions 158 | 159 | Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 160 | 161 | 10.2. Effect of New Versions 162 | 163 | You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 164 | 165 | 10.3. Modified Versions 166 | 167 | If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 168 | 169 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 170 | 171 | If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. 172 | 173 | Exhibit A - Source Code Form License Notice 174 | 175 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 176 | 177 | If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. 178 | 179 | You may add additional accurate notices of copyright ownership. 180 | 181 | Exhibit B - “Incompatible With Secondary Licenses” Notice 182 | 183 | This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # -= coming-soon =- 2 | 3 | [![MPL License](http://img.shields.io/badge/license-MPL-blue.svg?style=flat)](https://www.mozilla.org/MPL/2.0/) 4 | [![Build Status](http://img.shields.io/travis/SnootyMonkey/coming-soon.svg?style=flat)](https://travis-ci.org/SnootyMonkey/coming-soon) 5 | [![Dependency Status](https://www.versioneye.com/user/projects/5482dabc3f594e3aca000147/badge.svg?style=flat)](https://www.versioneye.com/user/projects/5482dabc3f594e3aca000147) 6 | [![Roadmap on Trello](http://img.shields.io/badge/roadmap-trello-blue.svg?style=flat)](https://trello.com/b/G8kY5MOf/coming-soon-https-github-com-snootymonkey-coming-soon) 7 | 8 | A simple landing page email collector, but with 90% more parentheses than the leading brand. 9 | 10 | 11 | ## What is coming-soon 12 | 13 | **coming-soon** is a simple "landing page" application that takes just a few minutes to setup on [Heroku](http://heroku.com/). With coming-soon you can quickly put up a page to publicize your new idea and collect email addresses of people who want to be notified when you are ready to launch. 14 | 15 | It's powered by Clojure, ClojureScript and Redis. 16 | 17 | You can see a [live demo](http://coming-soon-demo.heroku.com/) of coming-soon, or the screenshot below to get a sense of what it does. 18 | 19 | ![Silver Bullet Example](http://coming-soon-resources.s3.amazonaws.com/coming-soon-example.png) 20 | 21 | (Here is the coming-soon [configuration file](https://github.com/SnootyMonkey/coming-soon/blob/master/examples/silver_bullet/config.edn) for the Silver Bullet example above.) 22 | 23 | coming-soon is inspired by the Ruby app [LandingPad.rb](https://github.com/swanson/LandingPad.rb) by [Matt Swanson](https://github.com/swanson). 24 | 25 | 26 | ### What coming-soon can do 27 | 28 | Here are just some of the Fantastic FeaturesTM you can enjoy: 29 | 30 | * Captures email, when they signed up, and what website they came from 31 | * All the text on the landing page can be configured in a simple [config file](https://github.com/SnootyMonkey/coming-soon/blob/master/config.edn) 32 | * Most of the styles can be configured as well using the same [config file](https://github.com/SnootyMonkey/coming-soon/blob/master/config.edn) 33 | * User defined fonts, logo and background image using, you guessed it, the same [file](https://github.com/SnootyMonkey/coming-soon/blob/master/config.edn) 34 | * Drop in your code snippet from your web analytics provider, such as Google Analytics, and you can track views and conversion rates for signing up 35 | * Twitter, Facebook, GitHub and blog links are supported as a setting in the config file 36 | * Go beyond the config file and change all the HTML and CSS to put whatever you'd like on the page 37 | * The CSS is [Twitter Bootstrap](http://getbootstrap.com/) 38 | * The icons are [Font Awesome](http://fortawesome.github.io/Font-Awesome/) 39 | * All the third-party resources are hosted on a [CDN](http://en.wikipedia.org/wiki/Content_delivery_network) 40 | * coming-soon makes no attempt to promote itself on your page... that's just tacky 41 | * There's an [admin page](http://coming-soon-resources.s3.amazonaws.com/coming-soon-admin.png) to view sign-ups and export them as [JSON](http://en.wikipedia.org/wiki/JSON), [XML](http://en.wikipedia.org/wiki/XML) or [CSV](http://en.wikipedia.org/wiki/Comma-separated_values) 42 | * There's no code that's not Clojure or ClojureScript, so... there's that 43 | 44 | 45 | ### Why you should NOT use coming-soon 46 | 47 | Can you answer the question, "Why do you want to host your own landing page (even if Heroku is doing the hosting for you) with open source software rather than using a landing page service?" 48 | 49 | If you don't have a great answer to that question, then there are **lots** of fully hosted alternatives for landing pages, it's a crowded space for these services, so there is likely a better option for you in this list: 50 | 51 | * [Launch Rock](http://launchrock.co/) - the granddaddy of launch page services, tries to get users to share 52 | * [Unbounce](http://unbounce.com) - lots of templates, A/B testing, drag n' drop WYSIWYG, on the expensive side 53 | * [Strikingly](https://www.strikingly.com/) - for small general websites, but has email capture 54 | * [Kickofflabs](http://www.kickofflabs.com/) - fairly comprehensive, widgets for external integrations, email campaigns 55 | * [My Beta List](http://my.betali.st/) - also focused on user sharing 56 | * [Launch Effect](http://launcheffectapp.com/) - Wordpress theme 57 | * [Prefinery](http://www.prefinery.com/) - fairly comprehensive beta management, invite codes, viral sharing, feedback and surveys 58 | * [ooomf](http://ooomf.com) - for mobile apps 59 | * [LaunchGator](http://launch.deskgator.com/) 60 | * [Mailchimp](http://mailchimp.com) - you probably think of them as the email list back end only, but they can serve up the email capture page too 61 | 62 | 63 | ### Why you SHOULD use coming-soon 64 | 65 | It's very simple and it's very free. Just edit the config file, deploy it to Heroku for free, then get on with building your app. It's even faster than using many of the software as a service options. 66 | 67 | As an example, prior Silver Bullet landing page and the following [Falklandsophile](http://falklandsophile.com) and [IdeaFerret](http://ideaferret.com) landing pages were configured using just the configuration file with **no** custom HTML, CSS or coding. 68 | 69 | [![Falklandsophile Example](http://coming-soon-resources.s3.amazonaws.com/coming-soon-example2-small.png)](http://coming-soon-resources.s3.amazonaws.com/coming-soon-example2-full.png) 70 | 71 | (Here is the [configuration file](https://github.com/SnootyMonkey/coming-soon/blob/master/examples/falklandsophile/config.edn) for the Falklandsophile example.) 72 | 73 | [![IdeaFerret Example](http://coming-soon-resources.s3.amazonaws.com/coming-soon-example3-small.png)](http://coming-soon-resources.s3.amazonaws.com/coming-soon-example3-full.png) 74 | 75 | (Here is the [configuration file](https://github.com/SnootyMonkey/coming-soon/blob/master/examples/ideaferret/config.edn) for the IdeaFerret example.) 76 | 77 | If you want to go nuts on the templates and build some truly custom HTML and CSS for your landing page, then you have that option. You aren't locked into off-the-shelf templates or WYSIWYG editors or limited customization options like you are with many of the software as a service landing pages. 78 | 79 | Here is the same IdeaFerret landing page with just [this simple custom CSS](https://github.com/SnootyMonkey/coming-soon/blob/master/examples/ideaferret/css/custom.css). 80 | 81 | [![IdeaFerret Custom Example](http://coming-soon-resources.s3.amazonaws.com/coming-soon-example4-small.png)](http://coming-soon-resources.s3.amazonaws.com/coming-soon-example4-full.png) 82 | 83 | coming-soon is also open source (as in free to do whatever you like with it), so if it only does 80% of what you want, and you prefer to do the other 20% in Clojure/ClojureScript, it's probably for you. 84 | 85 | 86 | ## What it takes to use it 87 | 88 | coming-soon can be hosted on [Heroku](http://heroku.com) for free. Once you update the settings, just deploy it to Heroku and point a domain at it. That's all there is to it. You are then in business, and can get back to hacking on your app so that you have something to launch and send to all those email addresses you're collecting! 89 | 90 | 91 | ### Quick Start - 10 Steps to Heroku 92 | 93 | **1) Signup for Heroku and setup the Heroku tools on your machine.** 94 | 95 | If you haven't used Heroku before on your current computer, follow the [Getting Started with Heroku](https://devcenter.heroku.com/articles/quickstart) quick start steps to setup your free and instant account and the Heroku toolbelt on your local machine. 96 | 97 | **2) Download coming-soon from GitHub using git.** 98 | 99 | Run the following at the console. 100 | 101 | ```console 102 | git clone https://github.com/SnootyMonkey/coming-soon.git 103 | cd coming-soon 104 | ``` 105 | 106 | **3) Customize the configuration for your page.** 107 | 108 | Edit the file called **config.edn** in a text editor. 109 | 110 | You should see a **:coming-soon** block where you can enter admin access details for accessing your stored contacts. **PLEASE CHANGE YOUR USERNAME AND PASSWORD!** 111 | 112 | You should also see a **:landing-page** block where you can configure the appearance of your landing page. There are guide images for changing the [text](http://coming-soon-resources.s3.amazonaws.com/coming-soon-configuration-text.png), [fonts](http://coming-soon-resources.s3.amazonaws.com/coming-soon-configuration-fonts.png), [images](http://coming-soon-resources.s3.amazonaws.com/coming-soon-configuration-images.png), and [colors](http://coming-soon-resources.s3.amazonaws.com/coming-soon-configuration-colors.png). 113 | 114 | Update the copy to describe your application and change the colors, fonts, and other elements to customize your landing page. 115 | 116 | Replace the logo file **resources/public/img/logo.png** with your own logo, or edit the config file to include an empty string **""** rather than **"/img/logo.png"** for the **:logo** setting. 117 | 118 | If you want to use web analytics on your page, set the **:analytics** setting of the **:landing-page** block to **true**, and replace the contents of the **src/coming_soon/templates/analytics.html** file with the snippet of code provided by your analytics provider. 119 | 120 | **4) Commit the configuration changes for your app.** 121 | 122 | Once you have completed editing **config.edn** to add your app's settings, run the following commands in your coming-soon folder to commit the changes to your local [git](http://git-scm.com/) repository: 123 | 124 | ```console 125 | git add . 126 | git commit -m "Setting up my landing page." 127 | ``` 128 | 129 | **5) Create your Heroku app.** 130 | 131 | If you haven't used Heroku before on your current computer, login to Heroku from the commandline. Provide the email and password you used when creating your Heroku account when you are prompted for them. (If you have used Heroku from the commandline on your current computer, you can skip the login). 132 | 133 | ```console 134 | heroku login 135 | ``` 136 | 137 | Now create the new Heroku app and provide it a custom buildpack that can handle our ClojureScript compilation. 138 | 139 | ```console 140 | heroku create 141 | heroku config:add BUILDPACK_URL=https://github.com/kolov/heroku-buildpack-clojure 142 | ``` 143 | 144 | **6) Attach a Redis instance to your app.** 145 | 146 | As of this writing, there are 2 options for free Redis on Heroku. 147 | 148 | To use [Redis Cloud](https://addons.heroku.com/rediscloud), run this command: 149 | 150 | ```console 151 | heroku addons:add rediscloud:20 152 | ``` 153 | 154 | To use [Redis To Go](https://addons.heroku.com/redistogo), run this command: 155 | 156 | ```console 157 | heroku addons:add redistogo 158 | ``` 159 | As of this writing, there are 2 additional Redis options on Heroku that do not have a free tier. 160 | 161 | To use [openredis](https://addons.heroku.com/openredis), run this command: 162 | 163 | ```console 164 | heroku addons:add openredis:micro 165 | ``` 166 | 167 | To use [RedisGreen](https://addons.heroku.com/redisgreen), run this command: 168 | 169 | ```console 170 | heroku addons:add redisgreen:development 171 | ``` 172 | 173 | In your text editor comment in the **:redis-env-variable** line in the **:redis** section of the **config.edn** file that matches the Redis provider you selected. Then commit your change to git. 174 | 175 | ```console 176 | git add . 177 | git commit -m "Selecting my Redis provider." 178 | ``` 179 | 180 | **7) Push the code of your app up to the Heroku cloud.** 181 | 182 | ```console 183 | git push heroku master 184 | ``` 185 | 186 | **8) Congratulations! Now test out your landing page.** 187 | 188 | Launch your app in your browser. 189 | 190 | ```console 191 | heroku open 192 | ``` 193 | 194 | This will open a browser and you should see your landing page. Note the ugly URL Heroku created for you so you can get back to your page. 195 | 196 | Test Redis connectivity by adding /redis-test to your browser's URL after the URL for your app on Heroku 197 | 198 | For instance, if you app is **http://your-heroku-machine-name.heroku.com/** then you should load **http://your-heroku-machine-name.heroku.com/redis-test** 199 | 200 | You should see: "Connection to Redis is: OK" 201 | 202 | Go back to your landing page by removing the /redis-test from the URL and enter in your email address to test signing up for your own app. 203 | 204 | To view the contact information that's been entered into your landing page, navigate to **http://your-heroku-machine-name.heroku.com/contacts**. You will need to enter the admin username and password that you setup in **config.edn**. 205 | 206 | You should see a table listing the email and referral URL for everyone that has signed up for your app. 207 | 208 | [![Admin Example](http://coming-soon-resources.s3.amazonaws.com/coming-soon-admin-small.png)](http://coming-soon-resources.s3.amazonaws.com/coming-soon-admin-full.png) 209 | 210 | **9) Setup your custom domain.** 211 | 212 | You will probably want a custom domain rather than Heroku's default. Follow [Heroku's instructions](http://devcenter.heroku.com/articles/custom-domains) to setup your domain to point to your brand-new landing page. 213 | 214 | **10) Now get back to coding your app!** 215 | 216 | 217 | ### It looks good, but I want it to be more blue 218 | 219 | You can modify any of the HTML, CSS and images to customize your page. Just remember, you need to push any changes to Heroku so your live page will be updated. 220 | 221 | ```console 222 | git add . 223 | git commit -m 'Made the HTML and CSS more pretty.' 224 | git push heroku 225 | ``` 226 | 227 | 228 | ### It works well, but I want it to make my coffee too 229 | 230 | You can modify any of the Clojure and ClojureScript code to customize the behavior of your landing page. Just remember, you need to push any changes to Heroku so your live page will be updated. 231 | 232 | To build the ClojureScript code: 233 | 234 | ```console 235 | lein cljsbuild once 236 | ``` 237 | 238 | To watch the ClojureScript code and build it each time it changes: 239 | 240 | ```console 241 | lein cljsbuild auto 242 | ``` 243 | 244 | To run coming-soon locally: 245 | 246 | ```console 247 | lein start 248 | ``` 249 | 250 | ## Tests 251 | 252 | Tests are run in continuous integration of the `master` and `dev` branches on [Travis CI](https://travis-ci.org/SnootyMonkey/coming-soon): 253 | 254 | [![Build Status](http://img.shields.io/travis/SnootyMonkey/coming-soon.svg?style=flat)](https://travis-ci.org/SnootyMonkey/coming-soon) 255 | 256 | To run the tests locally: 257 | 258 | ```console 259 | lein test! 260 | ``` 261 | 262 | ## Frequently Asked Questions 263 | 264 | Here are some questions about coming-soon that I get asked all the time (or maybe I just made them all up). 265 | 266 | **Q:** Can I host it somewhere other than Heroku? 267 | **A:** Sure, nothing about coming-soon is Heroku specific. 268 | 269 | **Q:** Why is it so slow to load the landing page sometimes? 270 | **A:** Unless you are paying Heroku to host at least 2 dynos, you are subject to [idling](https://devcenter.heroku.com/articles/dynos#dyno-idling). This means that after an hour, your app will be spun down to save resources. The next unlucky soul to access your landing page will have to wait for 10 seconds or so for your app to spin back up. You can solve this by paying Heroku for 2 web dynos or by using a server monitoring service that pings your landing page more frequently than once an hour. 271 | 272 | **Q:** How many signups can I store before my Redis instance runs out of space? 273 | **A:** That's a very optimistic question. I like you; you've got gumption. A good rule of thumb is that an empty Redis instance is ~1MB and coming-soon uses (very conservatively) ~.5MB per 1,000 registrations. The .5MB per 1,000 registrations estimate is providing for a fairly long and unique referrer URL for every user, a fairly long email addresses for every user, and 25% overhead for memory fragmentation, so your actual results will likely be better. Assuming the conservative rule of thumb, and a free 20MB Redis service, you'll get over 38,000 registrations before it fills up and you have to start paying for Redis. 274 | 275 | **Q:** Who made this treasure? 276 | **A:** coming-soon is written by Sean Johnson, the founder of [Snooty Monkey](http://snootymonkey.com/). 277 | 278 | **Q:** How can I ever repay you for creating such a treasure? 279 | **A:** It's not required by the license terms, but please [drop me a note](http://snootymonkey.com/contact/) and let me know the URL of your landing page so I can take a look. 280 | 281 | **Q:** Can I use it for X? 282 | **A:** coming-soon is licensed with the [MIT license](https://github.com/SnootyMonkey/coming-soon/blob/master/MIT-LICENSE.txt), so you are free to use it pretty much however you'd like, in accordance with the license terms. 283 | 284 | **Q:** Can I add X to it? 285 | **A:** Sure, please fork coming-soon on GitHub if you'd like to enhance it. 286 | 287 | **Q:** Can I contribute my enhancements back to the project? 288 | **A:** Sure, send me your pull requests if you'd like to contribute back your enhancements. I promise to look at every pull request and incorporate it, or at least provide feedback on why if I won't. 289 | 290 | ## Links 291 | 292 | * [GitHub Project](http://github.com/SnootyMonkey/coming-soon) 293 | * [Change Log](http://github.com/SnootyMonkey/coming-soon/blob/master/CHANGELOG.md) 294 | * [Issue Tracker](http://github.com/SnootyMonkey/coming-soon/issues) 295 | * [Live Demo Example](http://coming-soon-demo.herokuapp.com) 296 | * [Live Admin Demo](http://coming-soon-admin.herokuapp.com/contacts) - login as admin / admin 297 | * [IdeaFerret](http://ideaferret.com) - a coming-soon powered landing page 298 | * [Falklandsophile](http://falklandsophile.com) - another coming-soon powered landing page 299 | * [OPENcompany](http://opencompany.io) - yet another coming-soon powered landing page 300 | 301 | ## License 302 | 303 | coming-soon is distributed under the [Mozilla Public License v2.0](http://www.mozilla.org/MPL/2.0/). 304 | 305 | Copyright © 2013-2017 [Snooty Monkey, LLC](http://snootymonkey.com/) 306 | --------------------------------------------------------------------------------