pyc-website

main website for pyc inc.

git clone https://9o.is/git/pyc-website.git

commit 77be8d3d7a1b8452f01390577cc32c84c4f52bfc
parent e9da5f0bf172a1ef4456d7b37c297429eee07ca8
Author: Jul <jul@9o.is>
Date:   Thu,  5 Jun 2014 19:25:39 -0400

added dropdown to topbar for logged in user and several other user snippets

Diffstat:
Msrc/main/scala/inc/pyc/snippet/UserSnip.scala | 59+++++++++++++++++++++++++++++++++++++++++++----------------
Dsrc/main/scala/inc/pyc/snippet/UserSnips.scala | 104-------------------------------------------------------------------------------
Msrc/main/scala/inc/pyc/snippet/UtilSnips.scala | 6------
Msrc/main/webapp/less/styles.less | 2+-
Msrc/main/webapp/templates-hidden/base-wrap.html | 19++++++++++++++++---
Msrc/main/webapp/vendor/ui-bootstrap-tpls.js | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
6 files changed, 225 insertions(+), 133 deletions(-)

diff --git a/src/main/scala/inc/pyc/snippet/UserSnip.scala b/src/main/scala/inc/pyc/snippet/UserSnip.scala @@ -14,26 +14,53 @@ import json._ import JsonDSL._ import JsonAST.{JValue, JString, JBool} import net.liftmodules.mongoauth.model.ExtSession +import net.liftmodules.extras.SnippetHelper /** - * Angular snippet when logged in. + * Basic User information from a snippet. */ -trait AngularUserSnippet extends AngularSnippet with AngularImplicits { - val user = User.currentUser - - protected def serve(snip: User => JValue): JValue = +sealed trait UserSnippet extends SnippetHelper with Logger { + + protected def user: Box[User] + + protected def serveNodeseq(snip: User => NodeSeq): NodeSeq = (for { u <- user ?~ "User not found" } yield { snip(u) - }): JValue - - protected def serveNodeseq(snip: User => NodeSeq): NodeSeq = + }): NodeSeq + + def username(in: NodeSeq): NodeSeq = serveNodeseq { user => + Text(user.username.get) + } + + def name(in: NodeSeq): NodeSeq = serveNodeseq { user => + val name = user.fname.get + " " + user.lname.get + if (name.length > 1) Text(name) + else Text(user.username.get) + } +} + +/** + * Basic logged in user information from a snippet. + */ +trait CurrentUser extends UserSnippet { + protected def user = User.currentUser +} + +object CurrentUser extends CurrentUser + +/** + * Angular snippet when logged in. + */ +trait AngularUserSnippet extends AngularSnippet with CurrentUser { + + protected def serve(snip: User => JValue): JValue = (for { u <- user ?~ "User not found" } yield { snip(u) - }): NodeSeq + }): JValue /** Validates and saves the currently signed in user. */ protected def validateAndSave(): JValue = serve { @@ -63,7 +90,7 @@ trait AngularUserSnippet extends AngularSnippet with AngularImplicits { } } -class UserLogin extends AngularSnippet with AngularImplicits { +class UserLogin extends AngularSnippet { def roundTrips: List[RoundTripInfo] = List("submit" -> submit _) @@ -98,7 +125,7 @@ class UserLogin extends AngularSnippet with AngularImplicits { } } -class PasswordChange extends AngularUserSnippet with AngularImplicits { +class PasswordChange extends AngularUserSnippet { /** * Passwords must match this pattern. @@ -136,7 +163,7 @@ class PasswordChange extends AngularUserSnippet with AngularImplicits { } } -class PasswordRecovery extends AngularSnippet with AngularImplicits { +class PasswordRecovery extends AngularSnippet { def roundTrips: List[RoundTripInfo] = List("submit" -> submit _) @@ -163,7 +190,7 @@ class PasswordRecovery extends AngularSnippet with AngularImplicits { } } -class UserRegistration extends AngularSnippet with AngularImplicits { +class UserRegistration extends AngularSnippet { def roundTrips: List[RoundTripInfo] = List("submit" -> submit _) @@ -201,7 +228,7 @@ class UserRegistration extends AngularSnippet with AngularImplicits { } } -class UserSettings extends AngularUserSnippet with AngularImplicits { +class UserSettings extends AngularUserSnippet { def roundTrips: List[RoundTripInfo] = List( "init" -> init _, @@ -248,7 +275,7 @@ class UserSettings extends AngularUserSnippet with AngularImplicits { } } -class UserSettingsEmail extends AngularUserSnippet with AngularImplicits { +class UserSettingsEmail extends AngularUserSnippet { def roundTrips: List[RoundTripInfo] = List( "submit" -> submit _, @@ -275,7 +302,7 @@ class UserSettingsEmail extends AngularUserSnippet with AngularImplicits { } } -class IdVerification extends AngularUserSnippet with AngularImplicits { +class IdVerification extends AngularUserSnippet { def roundTrips: List[RoundTripInfo] = List( "submit" -> submit _, diff --git a/src/main/scala/inc/pyc/snippet/UserSnips.scala b/src/main/scala/inc/pyc/snippet/UserSnips.scala @@ -1,104 +0,0 @@ -package inc.pyc -package snippet - -import config.Site -import model.{User, LoginCredentials} - -import scala.xml._ - -import net.liftweb._ -import common._ -import http.{DispatchSnippet, S, SHtml, StatefulSnippet} -import http.js.JsCmd -import http.js.JsCmds._ -import util._ -import Helpers._ - -import net.liftmodules.extras.{Gravatar, SnippetHelper} -import net.liftmodules.mongoauth.LoginRedirect -import net.liftmodules.mongoauth.model.ExtSession - -sealed trait UserSnippet extends SnippetHelper with Loggable { - - protected def user: Box[User] - - protected def serve(snip: User => NodeSeq): NodeSeq = - (for { - u <- user ?~ "User not found" - } yield { - snip(u) - }): NodeSeq - - protected def serve(html: NodeSeq)(snip: User => CssSel): NodeSeq = - (for { - u <- user ?~ "User not found" - } yield { - snip(u)(html) - }): NodeSeq - - def username(xhtml: NodeSeq): NodeSeq = serve { user => - Text(user.username.get) - } - - def name(xhtml: NodeSeq): NodeSeq = serve { user => - val name = user.fname.get + " " + user.lname.get - if (name.length > 1) Text(name) - else Text(user.username.get) - } - - def title(xhtml: NodeSeq): NodeSeq = serve { user => - <title data-lift="Menu.title">PYC: %*% - {user.username.get}</title> - } -} - -object CurrentUser extends UserSnippet { - protected def user = User.currentUser -} - -object ProfileLocUser extends UserSnippet { - - protected def user = Site.profileLoc.currentValue - - import java.text.SimpleDateFormat - - val df = new SimpleDateFormat("MMM d, yyyy") - - def profile(html: NodeSeq): NodeSeq = serve(html) { user => - val editLink: NodeSeq = - if (User.currentUser.filter(_.id.get == user.id.get).isDefined) - <a href={Site.editProfile.url} class="btn btn-info"><i class="icon-edit icon-white"></i> Edit Your Profile</a> - else - NodeSeq.Empty - - "#id_avatar *" #> Gravatar.imgTag(user.email.get) & - "#id_name *" #> <h3>{user.fname.get}</h3> & - "#id_whencreated" #> df.format(user.whenCreated.toDate).toString & - "#id_editlink *" #> editLink - } -} - -object UserTopbar { - def render = { - User.currentUser match { - case Full(user) => - <ul class="nav navbar-nav navbar-right" id="user"> - <li class="dropdown" data-dropdown="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown"> - {Gravatar.imgTag(user.email.get, 20)} - <span>{user.username.get}</span> - <b class="caret"></b> - </a> - <ul class="dropdown-menu"> - <li><a href={Site.profileLoc.calcHref(user)}><i class="icon-user"></i> Profile</a></li> - <li><lift:Menu.item name="Account" donthide="true" linktoself="true"><i class="icon-cog"></i> Settings</lift:Menu.item></li> - <li class="divider"></li> - <li><lift:Menu.item name="Logout" donthide="true"><i class="icon-off"></i> Log Out</lift:Menu.item></li> - </ul> - </li> - </ul> - case _ if (S.request.flatMap(_.location).map(_.name).filterNot(it => List("Login", "Register").contains(it)).isDefined) => - <a href="/login" class="btn btn-default navbar-btn">Sign In</a> - case _ => NodeSeq.Empty - } - } -} diff --git a/src/main/scala/inc/pyc/snippet/UtilSnips.scala b/src/main/scala/inc/pyc/snippet/UtilSnips.scala @@ -32,12 +32,6 @@ object ProductionOnly { else NodeSeq.Empty } -trait AngularImplicits { - implicit protected def listJvalueToJvalue(in: List[JValue]): JValue = { - in.headOption.getOrElse(JNull) - } -} - object Selector extends USStatesSelector object Social { diff --git a/src/main/webapp/less/styles.less b/src/main/webapp/less/styles.less @@ -27,7 +27,7 @@ // Components @import "@{BootstrapPath}/component-animations.less"; //@import "@{BootstrapPath}/glyphicons.less"; - //@import "@{BootstrapPath}/dropdowns.less"; +@import "@{BootstrapPath}/dropdowns.less"; //@import "@{BootstrapPath}/button-groups.less"; //@import "@{BootstrapPath}/input-groups.less"; @import "@{BootstrapPath}/navs.less"; diff --git a/src/main/webapp/templates-hidden/base-wrap.html b/src/main/webapp/templates-hidden/base-wrap.html @@ -47,9 +47,22 @@ <a data-lift="Menus.item?name=Login" class="btn btn-default btn-sm btn-topbar pull-right">Log In</a> <a data-lift="Menus.item?name=Register" class="btn btn-primary btn-topbar btn-sm text-white pull-right">Sign Up</a> </span> - <span data-lift="test_cond.loggedIn"> - <a href="/logout" class="btn btn-default btn-sm btn-topbar pull-right">Log Out</a> - </span> + <ul data-lift="test_cond.loggedIn" class="nav navbar-nav navbar-right"> + <li class="dropdown" on-toggle="toggled(open)"> + <button class="btn btn-default btn-sm btn-topbar dropdown-toggle" type="button" ng-disabled="disabled"> + <span data-lift="CurrentUser.name"></span> <b class="caret"></b> + </button> + <ul class="dropdown-menu" role="menu"> + <li> + <a data-lift="Menus.item?name=Settings" href="#">Settings</a> + </li> + <li class="divider"></li> + <li> + <a href="/logout">Logout</a> + </li> + </ul> + </li> + </ul> <a data-lift="Social.gplusLink" href="#" alt="Our Google+ Page" target="_blank"><i class="fa fa-google-plus fa-2x pull-right"></i></a> <a data-lift="Social.twitterLink" href="#" alt="Our Twitter Page" target="_blank"><i class="fa fa-twitter fa-2x pull-right"></i></a> <a data-lift="Social.facebookLink" href="#" alt="Our Facebook Page" target="_blank"><i class="fa fa-facebook fa-2x pull-right"></i></a> diff --git a/src/main/webapp/vendor/ui-bootstrap-tpls.js b/src/main/webapp/vendor/ui-bootstrap-tpls.js @@ -5,7 +5,7 @@ * Version: 0.11.0-SNAPSHOT - 2014-04-03 * License: MIT */ -angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.bindHtml","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.alert"]); +angular.module("ui.bootstrap", ["ui.bootstrap.tpls", "ui.bootstrap.bindHtml","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.alert","ui.bootstrap.dropdown"]); angular.module("ui.bootstrap.tpls", ["template/alert/alert.html"]); angular.module('ui.bootstrap.bindHtml', []) @@ -206,4 +206,167 @@ angular.module("template/alert/alert.html", []).run(["$templateCache", function( " <div ng-transclude></div>\n" + "</div>\n" + ""); -}]); -\ No newline at end of file +}]); + +/****************************** + * Dropdown + *****************************/ +angular.module('ui.bootstrap.dropdown', []) + +.constant('dropdownConfig', { + openClass: 'open' +}) + +.service('dropdownService', ['$document', function($document) { + var openScope = null; + + this.open = function( dropdownScope ) { + if ( !openScope ) { + $document.bind('click', closeDropdown); + $document.bind('keydown', escapeKeyBind); + } + + if ( openScope && openScope !== dropdownScope ) { + openScope.isOpen = false; + } + + openScope = dropdownScope; + }; + + this.close = function( dropdownScope ) { + if ( openScope === dropdownScope ) { + openScope = null; + $document.unbind('click', closeDropdown); + $document.unbind('keydown', escapeKeyBind); + } + }; + + var closeDropdown = function( evt ) { + var toggleElement = openScope.getToggleElement(); + if ( evt && toggleElement && toggleElement[0].contains(evt.target) ) { + return; + } + + openScope.$apply(function() { + openScope.isOpen = false; + }); + }; + + var escapeKeyBind = function( evt ) { + if ( evt.which === 27 ) { + openScope.focusToggleElement(); + closeDropdown(); + } + }; +}]) + +.controller('DropdownController', ['$scope', '$attrs', '$parse', 'dropdownConfig', 'dropdownService', '$animate', function($scope, $attrs, $parse, dropdownConfig, dropdownService, $animate) { + var self = this, + scope = $scope.$new(), // create a child scope so we are not polluting original one + openClass = dropdownConfig.openClass, + getIsOpen, + setIsOpen = angular.noop, + toggleInvoker = $attrs.onToggle ? $parse($attrs.onToggle) : angular.noop; + + this.init = function( element ) { + self.$element = element; + + if ( $attrs.isOpen ) { + getIsOpen = $parse($attrs.isOpen); + setIsOpen = getIsOpen.assign; + + $scope.$watch(getIsOpen, function(value) { + scope.isOpen = !!value; + }); + } + }; + + this.toggle = function( open ) { + return scope.isOpen = arguments.length ? !!open : !scope.isOpen; + }; + + // Allow other directives to watch status + this.isOpen = function() { + return scope.isOpen; + }; + + scope.getToggleElement = function() { + return self.toggleElement; + }; + + scope.focusToggleElement = function() { + if ( self.toggleElement ) { + self.toggleElement[0].focus(); + } + }; + + scope.$watch('isOpen', function( isOpen, wasOpen ) { + $animate[isOpen ? 'addClass' : 'removeClass'](self.$element, openClass); + + if ( isOpen ) { + scope.focusToggleElement(); + dropdownService.open( scope ); + } else { + dropdownService.close( scope ); + } + + setIsOpen($scope, isOpen); + if (angular.isDefined(isOpen) && isOpen !== wasOpen) { + toggleInvoker($scope, { open: !!isOpen }); + } + }); + + $scope.$on('$locationChangeSuccess', function() { + scope.isOpen = false; + }); + + $scope.$on('$destroy', function() { + scope.$destroy(); + }); +}]) + +.directive('dropdown', function() { + return { + restrict: 'CA', + controller: 'DropdownController', + link: function(scope, element, attrs, dropdownCtrl) { + dropdownCtrl.init( element ); + } + }; +}) + +.directive('dropdownToggle', function() { + return { + restrict: 'CA', + require: '?^dropdown', + link: function(scope, element, attrs, dropdownCtrl) { + if ( !dropdownCtrl ) { + return; + } + + dropdownCtrl.toggleElement = element; + + var toggleDropdown = function(event) { + event.preventDefault(); + + if ( !element.hasClass('disabled') && !attrs.disabled ) { + scope.$apply(function() { + dropdownCtrl.toggle(); + }); + } + }; + + element.bind('click', toggleDropdown); + + // WAI-ARIA + element.attr({ 'aria-haspopup': true, 'aria-expanded': false }); + scope.$watch(dropdownCtrl.isOpen, function( isOpen ) { + element.attr('aria-expanded', !!isOpen); + }); + + scope.$on('$destroy', function() { + element.unbind('click', toggleDropdown); + }); + } + }; +});