pyc-website
main website for pyc inc.
git clone https://9o.is/git/pyc-website.git
commit 9d3323be7983a09b7a0516f6954df8b2baf6b867 parent ec713740295338c441829ff66784a73d086fb179 Author: Jul <jul@9o.is> Date: Tue, 15 Jul 2014 19:11:41 -0400 added email to notify new atms; other fixes and recent bugs Diffstat:
| M | src/main/scala/inc/pyc/config/Site.scala | | | 2 | -- |
| M | src/main/scala/inc/pyc/model/SearchedLocation.scala | | | 5 | +++++ |
| A | src/main/scala/inc/pyc/model/field/MongoListFieldExtra.scala | | | 36 | ++++++++++++++++++++++++++++++++++++ |
| M | src/main/scala/inc/pyc/snippet/AtmSnip.scala | | | 57 | +++++++++++++++++++++++++++------------------------------ |
| M | src/main/webapp/app/App.js | | | 78 | ++++++++++++++++++++++++++++++------------------------------------------------ |
| M | src/main/webapp/app/controllers/Forms.js | | | 25 | +++++++++++++++---------- |
| M | src/main/webapp/faqs.html | | | 7 | +++---- |
| M | src/main/webapp/index.html | | | 16 | ++++++++-------- |
| M | src/main/webapp/less/pages/index.less | | | 14 | +++++++++----- |
| D | src/main/webapp/less/pages/locations.less | | | 85 | ------------------------------------------------------------------------------- |
| M | src/main/webapp/less/styles.less | | | 1 | - |
| D | src/main/webapp/locations.html | | | 31 | ------------------------------- |
| M | src/main/webapp/templates-hidden/parts/notify-atm-form.html | | | 78 | +++++++++++++++--------------------------------------------------------------- |
13 files changed, 148 insertions(+), 287 deletions(-)
diff --git a/src/main/scala/inc/pyc/config/Site.scala b/src/main/scala/inc/pyc/config/Site.scala @@ -41,7 +41,6 @@ object Site extends Locs { // Regular links val home = MenuLoc(Menu.i("Bitcoin ATM Services") / "index" >> TopBarGroup) - val locations = MenuLoc(Menu.i("ATM Locations") / "locations" >> TopBarGroup >> SiteMapGroup) val whatsBitcoin = MenuLoc(Menu.i("What Is Bitcoin") / "what-is-bitcoin" >> SiteMapGroup) val about = MenuLoc(Menu.i("About Us") / "about" >> TopBarGroup >> SiteMapGroup) val blog = MenuLoc(Menu.i("Blog") / "blog" >> TopBarGroup >> RedirectBlog >> SiteMapGroup) @@ -85,7 +84,6 @@ object Site extends Locs { private def menus = List( home.menu, - locations.menu, whatsBitcoin.menu, about.menu, blog.menu, diff --git a/src/main/scala/inc/pyc/model/SearchedLocation.scala b/src/main/scala/inc/pyc/model/SearchedLocation.scala @@ -1,8 +1,10 @@ package inc.pyc package model +import field._ import lib._ import net.liftweb._ +import common.Box import mongodb.record._ import mongodb.record.field._ import record.field._ @@ -27,6 +29,8 @@ class SearchedLocation private () extends MongoRecord[SearchedLocation] with Obj } } + /** List of emails that want to be notified when a new atm is near this search location. */ + object emails extends MongoListField[SearchedLocation, String](this) with MongoListFieldExtra[SearchedLocation, String] override def save(safe: Boolean = true) = { val qry = SearchedLocation.find(this) @@ -48,6 +52,7 @@ object SearchedLocation extends SearchedLocation with RogueMetaRecord[SearchedLo ensureIndex(loc.name -> "2d", unique = true) + def findByName(in: String): Box[SearchedLocation] = find(name.name, in) def find(search: SearchedLocation) = this.where(_.loc eqs search.loc.get) object usa extends CountryField(SearchedLocation.createRecord) { diff --git a/src/main/scala/inc/pyc/model/field/MongoListFieldExtra.scala b/src/main/scala/inc/pyc/model/field/MongoListFieldExtra.scala @@ -0,0 +1,35 @@ +package inc.pyc +package model +package field + +import com.mongodb._ +import net.liftweb._ +import common._ +import mongodb.record._ +import field._ + +trait MongoListFieldExtra[OwnerType <: BsonRecord[OwnerType], ListType] + extends MongoListField[OwnerType, ListType] { + + def add(a: ListType): OwnerType = + if (exists(a)) owner + else this(a :: get) + + def remove(a: ListType): OwnerType = + removeStr(a.toString) + + def removeStr(s: String): OwnerType = + this(get.filterNot(_.toString == s)) + + def exists(a: ListType): Boolean = + existsStr(a.toString) + + def existsStr(s: String): Boolean = + get.exists(_.toString == s) + + def find(a: ListType): Box[ListType] = + findStr(a.toString) + + def findStr(s: String): Box[ListType] = + get.find(_.toString == s) +} +\ No newline at end of file diff --git a/src/main/scala/inc/pyc/snippet/AtmSnip.scala b/src/main/scala/inc/pyc/snippet/AtmSnip.scala @@ -77,10 +77,14 @@ class AtmApplication extends AngularSnippet { } } -class FindAtm extends AngularSnippet { +class FindAtm extends AngularSnippet with Logger { + + var location: Option[SearchedLocation] = None def roundTrips: List[RoundTripInfo] = List( - "submit" -> submit _, "init" -> init _) + "submit" -> submit _, + "notifyEmail" -> notifyEmail _, + "init" -> init _) /** Distance to check for nearby ATM's */ val distance = 100 // kilometers (~62 miles) @@ -92,7 +96,12 @@ class FindAtm extends AngularSnippet { Geocode.geolocation(address)() match { case Right(geo) => - SearchedLocation.createRecord.name(address).loc(geo).save() + location = Some(SearchedLocation.findByName(address) openOr { + SearchedLocation.createRecord + }) map { + _.name(address).loc(geo).save() + } + val atms = Atm.nearby(geo, distance) if(atms.isEmpty) @@ -114,6 +123,21 @@ class FindAtm extends AngularSnippet { NgAlert.danger } + def notifyEmail(model: JValue): JValue = + for (JString(e) <- model \ "email") + yield location map { + loc => + val email = e.toLowerCase.trim + loc.emails.add(email).update + + NgAlert.success( + <i class="fa-fw fa fa-thumbs-o-up"></i> ++ + <span>Your notification has been received.</span> ++ + <p>We will notify you when there is an ATM nearby.</p> + ) + } getOrElse JNull + + /* Set featured ATM on map */ def init(o: JValue): JValue = { FeaturedAtm.atm.asJValue merge @@ -123,33 +147,6 @@ class FindAtm extends AngularSnippet { } -class NearAtmNotify extends AngularSnippet { - - def roundTrips: List[RoundTripInfo] = List("submit" -> submit _) - - def submit(model: JValue): JValue = { - val rec = NearAtmNotify.createRecord - rec.setFieldsFromJValue(model) - - rec.validate match { - case Nil => - rec.save(false) - NgAlert.success( - <i class="fa-fw fa fa-thumbs-o-up"></i> ++ - <span>Hello {rec.fname.get}, your notification has been received.</span> ++ - <p>We will notify you when there is an ATM near {rec.city.get}, {rec.state.get}.</p> - ) - case errors => - NgAlert.danger( - <i class="fa-fw fa fa-thumbs-o-down"></i> ++ - <span>Invalid submission</span>, - errors - ) - } - } -} - - sealed trait AtmSnip extends SnippetHelper with Logger { protected def atm: Box[Atm] diff --git a/src/main/webapp/app/App.js b/src/main/webapp/app/App.js @@ -15,39 +15,12 @@ var app = angular.module("app", [ 'ngReallyClick']); -var ZIP_CODE_REGEXP = /^(\d{5}(-\d{4})?|[A-Z]\d[A-Z] *\d[A-Z]\d)$/; var PASSWORD_REGEXP = /^(?=.*[^a-zA-Z])\S{8,}$/; var UNSAFE_URL_REGEXP = /[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/; var screen_xs = 480; var screen_md = 992; -app.controller('NearAtmNotifyAlert', ['$scope', '$controller', function($scope, $controller) { - $controller('AlertCtrl', {$scope: $scope}); - - $scope.$on('alertNearAtm', function(event, alert) { - $scope.addAlert(alert); - }); -}]); - -app.controller('NearAtmNotifyCtrl', ['$scope', '$controller', '$rootScope', function($scope, $controller, $rootScope) { - $controller('FormCtrl', {$scope: $scope}); - - $scope.zip_code_regex = ZIP_CODE_REGEXP; - - $scope.save = function() { - var success = function(alert) { - $rootScope.$broadcast('alertNearAtm', alert); - $scope.reset(); - }; - - var failure = function(alert) { - $rootScope.$broadcast('alertNearAtm', alert); - }; - - $scope.submit('NearAtmNotify', 'submit', success, failure); - }; -}]); app.controller('AtmApplicationAlert', ['$scope', '$controller', function($scope, $controller) { $controller('AlertCtrl', {$scope: $scope}); @@ -152,25 +125,6 @@ app.controller('UserLoginCtrl', ['$scope', '$controller', '$rootScope', function }; }]); -app.controller('FindAtmCtrl', ['$scope', '$state', '$rootScope', '$controller', function($scope, $state, $rootScope, $controller) { - $controller('FormCtrl', {$scope: $scope}); - - $scope.zip_code_regex = ZIP_CODE_REGEXP; - - $scope.search = function() { - var success = function() { - window.location.href = "/locations"; - $scope.reset(); - }; - - var failure = function(alert) { - $rootScope.$broadcast('alertDialog', alert); - }; - - $scope.submit('FindAtm', 'submit', success, failure); - }; -}]); - app.controller('PasswordChangeAlert', ['$scope', '$controller', function($scope, $controller) { $controller('AlertCtrl', {$scope: $scope}); @@ -458,7 +412,17 @@ app.controller('AtmHelperFuncs', ['$scope', function($scope) { }]); -app.controller('FindAtmCtrl', ['$scope', '$timeout', '$location', '$anchorScroll', '$controller', function($scope, $timeout, $location, $anchorScroll, $controller) { + +app.controller('NearAtmNotifyAlert', ['$scope', '$controller', function($scope, $controller) { + $controller('AlertCtrl', {$scope: $scope}); + + $scope.$on('alertNearAtm', function(event, alert) { + $scope.addAlert(alert); + }); +}]); + + +app.controller('FindAtmCtrl', ['$scope', '$timeout', '$location', '$anchorScroll', '$controller', '$rootScope', function($scope, $timeout, $location, $anchorScroll, $controller, $rootScope) { $controller('LoadedFormCtrl', {$scope: $scope, $controller: $controller}); $controller('AtmHelperFuncs', {$scope: $scope}); @@ -557,7 +521,7 @@ app.controller('FindAtmCtrl', ['$scope', '$timeout', '$location', '$anchorScroll if(window.innerWidth >= screen_md) { map.panBy(-150,-150); } else if(window.innerWidth >= screen_xs) { - map.panBy(0,-230); + map.panBy(0,-250); } else { map.panBy(0,-40); } @@ -592,6 +556,24 @@ app.controller('FindAtmCtrl', ['$scope', '$timeout', '$location', '$anchorScroll } }; + $scope.notification = {}; + + $scope.notifyEmail = function() { + var success = function(alert) { + $scope.notify_loading = false; + $rootScope.$broadcast('alertNearAtm', alert); + $scope.notification = {}; + }; + + var failure = function(alert) { + $scope.notify_loading = false; + $rootScope.$broadcast('alertNearAtm', alert); + }; + + $scope.notify_loading = true; + $scope.submit('FindAtm', 'notifyEmail', success, failure, $scope.notification, $scope.notify_loading); + }; + }]); diff --git a/src/main/webapp/app/controllers/Forms.js b/src/main/webapp/app/controllers/Forms.js @@ -22,22 +22,27 @@ angular.module("Forms", ['ngAlert']) /** * Success inputs for ng-class */ - $scope.stateSuccess = function(el) { - return "{'state-success':form."+el+".$valid && !form."+el+".$pristine}"; + $scope.stateSuccess = function(el, formName) { + var form = ((formName) ? formName : "form"); + + return "{'state-success':"+form+"."+el+".$valid && !"+form+"."+el+".$pristine}"; }; /** * Success and Failure inputs in ng-class */ - $scope.stateSuccessError = function(el) { - return "{'state-error':form."+el+".$invalid && !form."+el+".$pristine,'state-success':form."+el+".$valid && !form."+el+".$pristine}"; + $scope.stateSuccessError = function(el, formName) { + var form = ((formName) ? formName : "form"); + + return "{'state-error':"+form+"."+el+".$invalid && !"+form+"."+el+ + ".$pristine,'state-success':"+form+"."+el+".$valid && !"+form+"."+el+".$pristine}"; }; /** * Sends data to server. (Useful for Liftweb's roundtrip with ngAlert at least) */ - $scope.submit = function(className, funcName, successFunc, failureFunc, model) { - $scope.loading = true; + $scope.submit = function(className, funcName, successFunc, failureFunc, model, loading) { + $scope.loading = ((loading) ? false : true); var toSend = ((model) ? model : $scope.model); window[className][funcName](toSend).then(function(alert) { $scope.$apply(function() { @@ -59,8 +64,8 @@ angular.module("Forms", ['ngAlert']) * Resets client-side data and the entire form. */ $scope.reset = function() { - $scope.model = {}; - $scope.form.$setPristine(); + $scope.model = {}; + $scope.form.$setPristine(); }; }]) @@ -145,7 +150,7 @@ angular.module("Forms", ['ngAlert']) */ $scope.reset = function() { $scope.model = {}; - $scope.form.$setPristine(); - $scope.model= angular.copy($scope.master); + $scope.form.$setPristine(); + $scope.model = angular.copy($scope.master); }; }]); \ No newline at end of file diff --git a/src/main/webapp/faqs.html b/src/main/webapp/faqs.html @@ -17,10 +17,9 @@ <li class="padding-top-10"> <h3 class="bold">In which states is PYC available?</h3> <p> - We established our first machine at Hellas Bakery, Schenectady, NY in June 27. - ATMs may soon be available in other areas -- stay tuned. If you’re located in - another state but would like to be the next customer to benefit from our service, - <a href="/locations">fill out this form so we can know you’re interested</a>. If + We established our first machine at Hellas Bakery, Albany, NY in June 27. + ATMs may soon be available in other areas -- stay tuned. If you’re interested in + using our service, <a href="/##blusep">search for an ATM near your area</a>. If you're a merchant and interested in having us host an ATM in your establishment, <a href="/##index-cols">fill out the ATM application in the front page</a>. </p> diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html @@ -31,20 +31,21 @@ <div id="atm-map" ng-class="{'nonfound': nonfound}" class="row" ng-click="expandMap()"> <div class="col-xs-12"> <div class="margin-xs-fix"> + <div ng-show="nonfound" class="layer-info" ng-cloak> <div class="row"> - <div class="col-xs-12 col-md-6 col-lg-6"> + <div class="col-xs-10 col-md-6 col-xs-offset-1 col-md-offset-0"> <div id="coming-soon-msg" class="fade-in"> - <h1 class="text-white">ATMs Coming Soon</h1> + <h1 class="text-white">ATM Coming Soon</h1> <p class="message text-white"> - There are no Bitcoin ATMs near your location. - Fill out the form to receive a notification - when an ATM is near your home town. + There is no Bitcoin ATM near your location. + Fill out your email address to receive a notification + when an ATM is near your hometown. </p> </div> </div> - <div - class="col-xs-10 col-md-5 col-lg-4 col-xs-offset-1 col-md-offset-0 col-lg-offset-1"> + <div class="col-md-12 visible-md visible-lg"></div> + <div class="col-xs-10 col-sm-4 col-md-6 col-xs-offset-1 col-sm-offset-4 col-md-offset-0"> <span data-lift="embed?what=/templates-hidden/parts/notify-atm-form"></span> </div> </div> @@ -72,7 +73,6 @@ <div class="input-group-btn"> <button class="btn btn-default btn-primary" type="submit" ng-disabled="form.$invalid" disabler ng-model="loading"> - <i class="fa fa-search"></i> Search ATM </button> </div> diff --git a/src/main/webapp/less/pages/index.less b/src/main/webapp/less/pages/index.less @@ -28,8 +28,8 @@ @atm-map-height-sm: 600px; // nonfound - @atm-map-height-nonfound-sm: @atm-map-height + 250; - @atm-map-height-nonfound-xs: @atm-map-height + 450; + @atm-map-height-nonfound-sm: @atm-map-height; // + 250; + @atm-map-height-nonfound-xs: @atm-map-height; // + 450; @atm-map-margin : @atm-map-height / 20; @@ -132,7 +132,7 @@ height: @atm-map-height-sm; } } - + &.nonfound { .angular-google-map-container, .layer-overlay, layer-info { @@ -145,7 +145,7 @@ } } } - + .layer-overlay { position: absolute; @@ -202,6 +202,10 @@ font-size: @message-font-size; padding: 0 @atm-map-margin; } + + @media (min-width: @screen-sm) { + padding-bottom: 20px; + } } } } @@ -427,7 +431,7 @@ } @media(max-width: 600px) { - top: @map-open-margin-nonfound-sm + 200; + top: @map-open-margin-nonfound-sm; } @media(max-width: @screen-xxs-max) { diff --git a/src/main/webapp/less/pages/locations.less b/src/main/webapp/less/pages/locations.less @@ -1,84 +0,0 @@ -#atm-locations { - position: relative; - - // WARNING: Width is fixed because of layer-outer, but map-canvas can be 100% width - // layer-outer is temporary :) - .angular-google-map-container, .layer-overlay, layer-info { - pointer-events: none; - - @media (max-width: @screen-lg) { - width: 1136px; - height: 400px; - } - - @media (max-width: @screen-md-max) { - width: 936px; - height: 400px; - } - - @media (max-width: @screen-sm-max) { - width: 716px; - height: 650px; - } - - @media (max-width: @screen-xs-max) { - width: 98%; - height: 700px; - } - - width: 1136px; - height: 400px; - } - - .layer-overlay { - position: absolute; - - .layer-inner { - #gradient > .horizontal-three-colors(rgba(0,0,0,0.6), rgba(0,0,0,0.3), 50%, rgba(0,0,0,0.6)); - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: @gmap-overlay-zindex; - } - } - - .layer-info { - position: absolute; - z-index: @gmap-text-overlay-zindex + 10; - - #coming-soon-msg { - @title-font-size: 50px; - @message-font-size: 20px; - - @media (max-width: @screen-sm-max) { - .text-center(); - } - - h1 { - @media (max-width: @screen-xxs-max) { - font-size: @title-font-size * 0.75; - padding: 5px; - } - - @media (min-width: @screen-xs) { - font-size: @title-font-size; - padding: 20px; - } - } - - p.message { - @media (max-width: @screen-xxs-max){ - font-size: @message-font-size * 0.85; - padding: 5px; - } - - @media (min-width: @screen-xs) { - font-size: @message-font-size; - padding: 20px; - } - } - } - } -} -\ No newline at end of file diff --git a/src/main/webapp/less/styles.less b/src/main/webapp/less/styles.less @@ -152,7 +152,6 @@ @import "@{PagesPath}/about.less"; @import "@{PagesPath}/index.less"; -@import "@{PagesPath}/locations.less"; @import "@{PagesPath}/purchase_limit.less"; @import "@{PagesPath}/atm.less"; diff --git a/src/main/webapp/locations.html b/src/main/webapp/locations.html @@ -1,30 +0,0 @@ -<div data-lift="surround?with=base-wrap&at=content"> - - <div id="atm-locations"> - <div class="layer-info"> - <div class="row margin-top-10"> - <div class="col-xs-12 col-md-6 col-lg-6"> - <div id="coming-soon-msg"> - <h1 class="text-white">ATMs Coming Soon</h1> - <p class="message text-white"> - Fill the form to get notified when an ATM comes to your home town. - </p> - </div> - </div> - <div class="col-xs-10 col-md-5 col-lg-4 col-xs-offset-1 col-md-offset-0 col-lg-offset-1"> - <span data-lift="embed?what=/templates-hidden/parts/notify-atm-form"></span> - </div> - </div> - </div> - - <div class="layer-overlay"> - <div class="layer-inner"></div> - </div> - - <div ng-controller="GMapCtrl"> - <google-map center="map.center" zoom="map.zoom" draggable="false" options="{disableDefaultUI:true}"></google-map> - </div> - - </div> - -</div> -\ No newline at end of file diff --git a/src/main/webapp/templates-hidden/parts/notify-atm-form.html b/src/main/webapp/templates-hidden/parts/notify-atm-form.html @@ -1,68 +1,20 @@ -<div id="notify-atm-form" data-lift="NearAtmNotify" ng-controller="NearAtmNotifyCtrl" class="boxshadow" ng-cloak> - <form name="form" class="smart-form client-form" ng-submit="save()" novalidate> - <header>Notify me when there is a nearby ATM</header> - <fieldset> - <div class="row"> - <section class="col col-6"> - <label class="input" ng-class="{{ stateSuccess('fname') }}"> - <i class="icon-append fa fa-user"></i> - <input name="fname" ng-model="model.fname" placeholder="First name" type="text" autofocus> - <b class="tooltip tooltip-top-left">Enter your first name.</b> - </label> - </section> - <section class="col col-6"> - <label class="input" ng-class="{{ stateSuccess('lname') }}"> - <i class="icon-append fa fa-user"></i> - <input name="lname" ng-model="model.lname" placeholder="Last name" type="text"> - <b class="tooltip tooltip-top-left">Enter your last name.</b> - </label> - </section> - </div> +<div id="notify-atm-form" class="boxshadow fade-in"> - <div class="row"> - <section class="col col-6"> - <label class="input" ng-class="{{ stateSuccessError('email') }}"> - <i class="icon-append fa fa-envelope-o"></i> - <input name="email" ng-model="model.email" placeholder="E-mail" type="email" required> - <b class="tooltip tooltip-top-left">Enter your email address so we can notify you.</b> - <span ng-show="form.email.$invalid && form.email.$dirty" class="text-danger small"> - Invalid email address - </span> - </label> - </section> + <form name="notify" ng-submit="notifyEmail()" novalidate> + <div class="input-group"> + <input required class="form-control" type="email" name="email" + placeholder="E-mail Address" ng-model="notification.email" + ng-maxlength="100"></input> - <section class="col col-6"> - <label class="input" ng-class="{{ stateSuccessError('postal') }}"> - <input name="postal" ng-model="model.postal" placeholder="Post code" type="text" ng-pattern="zip_code_regex" required/> - <b class="tooltip tooltip-top-left">Enter the zip code you want the ATM to be located.</b> - <span ng-show="form.postal.$error.pattern" class="text-danger small"> - Invalid postal code - </span> - </label> - </section> - </div> - <div class="row"> - <section class="col col-6"> - <label class="input" ng-class="{{ stateSuccess('city') }}"> - <input name="city" ng-model="model.city" placeholder="City" type="text"> - <b class="tooltip tooltip-top-left">Please provide the city for the ATM.</b> - </label> - </section> - <section class="col col-6"> - <label data-lift="Selector.states" class="select" ng-class="{{ stateSuccessError('state') }}"> - <select name="state" ng-model="model.state"></select> - <i></i> - </label> - </section> + <div class="input-group-btn"> + <button class="btn btn-default btn-primary" type="submit" disabler ng-model="notify_loading"> + <i class="fa fa-envelope-o"></i> Notify Me + </button> </div> - </fieldset> - - <footer> - <div ng-controller="NearAtmNotifyAlert" ng-cloak> - <span data-lift="embed?what=/templates-hidden/parts/alert"></span> - </div> - - <button type="submit" class="btn btn-primary" ng-disabled="form.$invalid" disabler ng-model="loading">Notify Me</button> - </footer> + </div> + <div ng-controller="NearAtmNotifyAlert"> + <span data-lift="embed?what=/templates-hidden/parts/alert"></span> + </div> </form> + </div> \ No newline at end of file