├── Procfile ├── .godir ├── .gitignore ├── public ├── img │ ├── no-family.png │ ├── google-plus.png │ ├── no-followers.png │ ├── no-friends.png │ ├── no-problem.png │ ├── helping-hands.png │ └── favicon │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon-96x96.png ├── css │ ├── fonts │ │ ├── Muli.woff │ │ ├── Muli.woff2 │ │ ├── Monoton.woff │ │ ├── Monoton.woff2 │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── style.scss │ ├── _create-service.scss │ ├── _search.scss │ ├── _account.scss │ ├── _landing.scss │ ├── _layout.scss │ ├── _common.scss │ ├── _service.scss │ └── style.css.map ├── robots.txt └── scripts │ ├── simple-page.jsx │ ├── vendor │ ├── ZeroClipboard.swf │ └── jquery.ba-throttle-debounce.min.js │ ├── simple-page.js │ ├── create-service-home.jsx │ ├── service-home.jsx │ ├── search-home.jsx │ ├── create-service-home.js │ ├── service-home.js │ ├── search-home.js │ ├── noty-theme.js │ ├── create-service.jsx │ ├── common.jsx │ ├── create-service.js │ ├── common.js │ ├── search.jsx │ ├── search.js │ ├── landing-home.jsx │ ├── account.jsx │ ├── account.js │ ├── landing-home.js │ ├── service.jsx │ └── service.js ├── Dockerfile ├── views ├── account.html ├── index.html ├── create-service.html ├── service.html ├── search.html ├── 404.html └── layout.html ├── web ├── middleware │ ├── database.go │ └── authenticator.go └── server.go ├── docker-compose.yml ├── utils ├── current_user_accessor.go ├── database_accessor.go ├── session_manager.go └── base_page_creator.go ├── README.md ├── main.go ├── models ├── referral_code_flag.go ├── analytics.go ├── user.go ├── service.go └── referral_code.go └── controllers ├── sitemap_controller.go ├── create_service_controller.go ├── search_controller.go ├── service_controller.go ├── referral_code_controller.go └── account_controller.go /Procfile: -------------------------------------------------------------------------------- 1 | web: refermadness -------------------------------------------------------------------------------- /.godir: -------------------------------------------------------------------------------- 1 | github.com/larryprice/refermadness -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .godeps 2 | gin-bin 3 | .sass-cache 4 | .module-cache 5 | .env -------------------------------------------------------------------------------- /public/img/no-family.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/no-family.png -------------------------------------------------------------------------------- /public/css/fonts/Muli.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/css/fonts/Muli.woff -------------------------------------------------------------------------------- /public/css/fonts/Muli.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/css/fonts/Muli.woff2 -------------------------------------------------------------------------------- /public/img/google-plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/google-plus.png -------------------------------------------------------------------------------- /public/img/no-followers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/no-followers.png -------------------------------------------------------------------------------- /public/img/no-friends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/no-friends.png -------------------------------------------------------------------------------- /public/img/no-problem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/no-problem.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # robots.txt for https://www.refer-madness.com/ 2 | 3 | User-agent: * 4 | Disallow: /search -------------------------------------------------------------------------------- /public/css/fonts/Monoton.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/css/fonts/Monoton.woff -------------------------------------------------------------------------------- /public/img/helping-hands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/helping-hands.png -------------------------------------------------------------------------------- /public/css/fonts/Monoton.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/css/fonts/Monoton.woff2 -------------------------------------------------------------------------------- /public/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/favicon/favicon.ico -------------------------------------------------------------------------------- /public/scripts/simple-page.jsx: -------------------------------------------------------------------------------- 1 | React.render( 2 |
, 3 | document.getElementById('content') 4 | ); -------------------------------------------------------------------------------- /public/img/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/img/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /public/scripts/vendor/ZeroClipboard.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/scripts/vendor/ZeroClipboard.swf -------------------------------------------------------------------------------- /public/scripts/simple-page.js: -------------------------------------------------------------------------------- 1 | React.render( 2 | React.createElement(Header, {smallTitle: true}), 3 | document.getElementById('content') 4 | ); -------------------------------------------------------------------------------- /public/css/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/css/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/css/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/css/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/css/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/css/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/css/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larryprice/refermadness/HEAD/public/css/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/css/style.scss: -------------------------------------------------------------------------------- 1 | @import 'common'; 2 | @import 'layout'; 3 | @import 'search'; 4 | @import 'service'; 5 | @import 'landing'; 6 | @import 'create-service'; 7 | @import 'account'; -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.4 2 | 3 | RUN go get github.com/codegangsta/gin 4 | 5 | ADD . /go/src/github.com/larryprice/refermadness 6 | WORKDIR /go/src/github.com/larryprice/refermadness 7 | RUN go get 8 | -------------------------------------------------------------------------------- /views/account.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 |
3 | 4 | 5 | 6 | {{end}} -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | {{end}} -------------------------------------------------------------------------------- /views/create-service.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | {{end}} -------------------------------------------------------------------------------- /views/service.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | {{end}} -------------------------------------------------------------------------------- /views/search.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | {{end}} -------------------------------------------------------------------------------- /web/middleware/database.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/codegangsta/negroni" 5 | "github.com/larryprice/refermadness/utils" 6 | "net/http" 7 | ) 8 | 9 | type Database struct { 10 | da utils.DatabaseAccessor 11 | } 12 | 13 | func NewDatabase(da utils.DatabaseAccessor) *Database { 14 | return &Database{da} 15 | } 16 | 17 | func (d *Database) Middleware() negroni.HandlerFunc { 18 | return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 19 | reqSession := d.da.Clone() 20 | defer reqSession.Close() 21 | d.da.Set(r, reqSession) 22 | next(rw, r) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | main: 2 | build: . 3 | command: gin run 4 | env_file: .env 5 | volumes: 6 | - ./:/go/src/github.com/larryprice/refermadness 7 | working_dir: /go/src/github.com/larryprice/refermadness 8 | ports: 9 | - "3000:3000" 10 | links: 11 | - db 12 | sass: 13 | image: larryprice/sass 14 | volumes: 15 | - ./public/css:/src 16 | jsx: 17 | image: larryprice/jsx 18 | volumes: 19 | - ./public/scripts:/src 20 | db: 21 | image: mongo:3.0 22 | command: mongod --smallfiles --quiet --logpath=/dev/null 23 | volumes_from: 24 | - dbvolume 25 | dbvolume: 26 | image: busybox:ubuntu-14.04 27 | volumes: 28 | - /data/db -------------------------------------------------------------------------------- /views/404.html: -------------------------------------------------------------------------------- 1 | {{define "body"}} 2 |
3 |
4 | 5 |
6 |
7 |
8 |

9 | The page you are looking for could not be found. 10 |

11 |

12 | How about you just return to the home page and pretend this never happened. 13 |

14 |
15 |
16 |
17 | 18 | 19 | 20 | {{end}} -------------------------------------------------------------------------------- /utils/current_user_accessor.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/gorilla/context" 5 | "github.com/larryprice/refermadness/models" 6 | "net/http" 7 | ) 8 | 9 | type CurrentUserAccessor struct { 10 | key int 11 | } 12 | 13 | func NewCurrentUserAccessor(key int) *CurrentUserAccessor { 14 | return &CurrentUserAccessor{key} 15 | } 16 | 17 | func (cua *CurrentUserAccessor) Set(r *http.Request, user *models.User) { 18 | context.Set(r, cua.key, user) 19 | } 20 | 21 | func (cua *CurrentUserAccessor) Clear(r *http.Request) { 22 | context.Delete(r, cua.key) 23 | } 24 | 25 | func (cua *CurrentUserAccessor) Get(r *http.Request) *models.User { 26 | if rv := context.Get(r, cua.key); rv != nil { 27 | return rv.(*models.User) 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /public/scripts/vendor/jquery.ba-throttle-debounce.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery throttle / debounce - v1.1 - 3/7/2010 3 | * http://benalman.com/projects/jquery-throttle-debounce-plugin/ 4 | * 5 | * Copyright (c) 2010 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | (function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Refer Madness 2 | ============= 3 | 4 | [Refer Madness](https://www.refer-madness.com) is a web application for finding and submitting referral codes for any subscription service. The goal of this application is to replace the "friend" from "Refer-a-Friend" with "stranger" or "anonymous internet person." 5 | 6 | Backend is written in Go, frontend is largely ReactJS and SASS. The easiest way to compile and run the app is with [docker-compose](https://github.com/docker/compose): 7 | 8 | ``` 9 | $ docker-compose build 10 | $ docker-compose up 11 | ``` 12 | 13 | Which will download the application dependencies and then watch the SASS, JSX, and Go directories for changes and recompile as necessary. A new dependency means re-running `docker-compose build` to generate a new refermadness image. 14 | -------------------------------------------------------------------------------- /utils/database_accessor.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/gorilla/context" 5 | "gopkg.in/mgo.v2" 6 | "net/http" 7 | ) 8 | 9 | type DatabaseAccessor struct { 10 | *mgo.Session 11 | url string 12 | name string 13 | key int 14 | } 15 | 16 | func NewDatabaseAccessor(url, name string, key int) *DatabaseAccessor { 17 | session, _ := mgo.Dial(url) 18 | session.DB(name).C("service").EnsureIndex(mgo.Index{Key: []string{"search"}}) 19 | 20 | return &DatabaseAccessor{session, url, name, key} 21 | } 22 | 23 | func (d *DatabaseAccessor) Set(r *http.Request, db *mgo.Session) { 24 | context.Set(r, d.key, db.DB(d.name)) 25 | } 26 | 27 | func (d *DatabaseAccessor) Get(r *http.Request) *mgo.Database { 28 | if rv := context.Get(r, d.key); rv != nil { 29 | return rv.(*mgo.Database) 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/larryprice/refermadness/utils" 5 | "github.com/larryprice/refermadness/web" 6 | "github.com/stretchr/graceful" 7 | "os" 8 | ) 9 | 10 | func main() { 11 | isDevelopment := os.Getenv("ENVIRONMENT") == "development" 12 | dbURL := os.Getenv("MONGOLAB_URI") 13 | if isDevelopment { 14 | dbURL = os.Getenv("DB_PORT_27017_TCP_ADDR") 15 | } 16 | 17 | dbAccessor := utils.NewDatabaseAccessor(dbURL, os.Getenv("DATABASE_NAME"), 0) 18 | cuAccessor := utils.NewCurrentUserAccessor(1) 19 | s := web.NewServer(*dbAccessor, *cuAccessor, os.Getenv("GOOGLE_OAUTH2_CLIENT_ID"), 20 | os.Getenv("GOOGLE_OAUTH2_CLIENT_SECRET"), os.Getenv("SESSION_SECRET"), 21 | isDevelopment, os.Getenv("GOOGLE_ANALYTICS_KEY")) 22 | 23 | port := os.Getenv("PORT") 24 | if port == "" { 25 | port = "3000" 26 | } 27 | 28 | graceful.Run(":"+port, 0, s) 29 | } 30 | -------------------------------------------------------------------------------- /models/referral_code_flag.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | "time" 7 | ) 8 | 9 | type ReferralCodeFlag struct { 10 | ID bson.ObjectId `bson:"_id"` 11 | CodeID bson.ObjectId `bson:"code_id"` 12 | ReporterID bson.ObjectId `bson:"reporter_id"` 13 | 14 | // Analytics 15 | DateReported time.Time `bson:"date_reported"` 16 | } 17 | 18 | func NewReferralCodeFlag(codeID, userID bson.ObjectId) *ReferralCodeFlag { 19 | return &ReferralCodeFlag{ 20 | ID: bson.NewObjectId(), 21 | CodeID: codeID, 22 | ReporterID: userID, 23 | DateReported: time.Now(), 24 | } 25 | } 26 | 27 | func (c *ReferralCodeFlag) Save(db *mgo.Database) error { 28 | _, err := c.coll(db).UpsertId(c.ID, c) 29 | return err 30 | } 31 | 32 | func (*ReferralCodeFlag) coll(db *mgo.Database) *mgo.Collection { 33 | return db.C("referral_code_flag") 34 | } 35 | -------------------------------------------------------------------------------- /utils/session_manager.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/goincremental/negroni-sessions" 5 | "net/http" 6 | ) 7 | 8 | type SessionManager interface { 9 | Get(*http.Request, string) string 10 | Set(*http.Request, string, string) 11 | Delete(*http.Request, string) 12 | } 13 | 14 | type SessionManagerImpl struct { 15 | } 16 | 17 | func NewSessionManager() *SessionManagerImpl { 18 | return &SessionManagerImpl{} 19 | } 20 | 21 | func (sa *SessionManagerImpl) Get(req *http.Request, key string) string { 22 | if val := sessions.GetSession(req).Get(key); val != nil { 23 | return val.(string) 24 | } 25 | 26 | return "" 27 | } 28 | 29 | func (sa *SessionManagerImpl) Set(req *http.Request, key, value string) { 30 | sessions.GetSession(req).Set(key, value) 31 | } 32 | 33 | func (sa *SessionManagerImpl) Delete(req *http.Request, key string) { 34 | sessions.GetSession(req).Delete(key) 35 | } 36 | -------------------------------------------------------------------------------- /utils/base_page_creator.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type BasePageCreator interface { 8 | Get(*http.Request) BasePage 9 | } 10 | 11 | type BasePageCreatorImpl struct { 12 | currentUser CurrentUserAccessor 13 | gaKey string 14 | } 15 | 16 | func NewBasePageCreator(currentUser CurrentUserAccessor, gaKey string) *BasePageCreatorImpl { 17 | return &BasePageCreatorImpl{ 18 | currentUser: currentUser, 19 | gaKey: gaKey, 20 | } 21 | } 22 | 23 | type BasePage struct { 24 | LoggedIn bool 25 | Username string 26 | AnalyticsKey string 27 | } 28 | 29 | func (bp *BasePageCreatorImpl) Get(r *http.Request) BasePage { 30 | if user := bp.currentUser.Get(r); user != nil { 31 | return BasePage{ 32 | LoggedIn: user != nil, 33 | Username: user.Email, 34 | AnalyticsKey: bp.gaKey, 35 | } 36 | } 37 | return BasePage{ 38 | LoggedIn: false, 39 | Username: "", 40 | AnalyticsKey: bp.gaKey, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/scripts/create-service-home.jsx: -------------------------------------------------------------------------------- 1 | var ServicePanel = React.createClass({ 2 | render: function() { 3 | return ( 4 |
5 |
6 | 7 |
8 |
9 | ); 10 | } 11 | }); 12 | 13 | var CreateServiceHome = React.createClass({ 14 | render: function() { 15 | var waitToPop = /^((?!chrome).)*safari/i.test(navigator.userAgent); 16 | $(window).off("popstate").on("popstate", function() { 17 | if (waitToPop) { 18 | waitToPop = false; 19 | return; 20 | } 21 | window.location = window.location.href; 22 | }); 23 | 24 | return ( 25 |
26 |
27 | 28 |
29 | ); 30 | } 31 | }); 32 | 33 | React.render( 34 | , 35 | document.getElementById('content') 36 | ); -------------------------------------------------------------------------------- /public/scripts/service-home.jsx: -------------------------------------------------------------------------------- 1 | var ServicePanel = React.createClass({ 2 | render: function() { 3 | return ( 4 |
5 |
6 | 7 |
8 |
9 | ); 10 | } 11 | }); 12 | 13 | var ServiceHome = React.createClass({ 14 | render: function() { 15 | var waitToPop = /^((?!chrome).)*safari/i.test(navigator.userAgent); 16 | $(window).off("popstate").on("popstate", function() { 17 | if (waitToPop) { 18 | waitToPop = false; 19 | return; 20 | } 21 | window.location = window.location.href; 22 | }); 23 | 24 | return ( 25 |
26 |
27 | 28 |
29 | ); 30 | } 31 | }); 32 | 33 | React.render( 34 | , 35 | document.getElementById('content') 36 | ); -------------------------------------------------------------------------------- /public/scripts/search-home.jsx: -------------------------------------------------------------------------------- 1 | var SearchPanel = React.createClass({ 2 | render: function() { 3 | return ( 4 |
5 |
6 | 7 |
8 |
9 | ); 10 | } 11 | }); 12 | 13 | var SearchHome = React.createClass({ 14 | componentDidMount: function() { 15 | $(".create-search-result").removeClass("hidden"); 16 | }, 17 | render: function() { 18 | var waitToPop = /^((?!chrome).)*safari/i.test(navigator.userAgent); 19 | $(window).off("popstate").on("popstate", function() { 20 | if (waitToPop) { 21 | waitToPop = false; 22 | return; 23 | } 24 | window.location = window.location.href; 25 | }); 26 | 27 | return ( 28 |
29 |
30 | 31 |
32 | ); 33 | } 34 | }); 35 | 36 | React.render( 37 | , 38 | document.getElementById('content') 39 | ); -------------------------------------------------------------------------------- /web/middleware/authenticator.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/codegangsta/negroni" 5 | "github.com/larryprice/refermadness/models" 6 | "github.com/larryprice/refermadness/utils" 7 | "gopkg.in/mgo.v2/bson" 8 | "net/http" 9 | ) 10 | 11 | type Authenticator struct { 12 | currentUser utils.CurrentUserAccessor 13 | database utils.DatabaseAccessor 14 | session utils.SessionManager 15 | } 16 | 17 | func NewAuthenticator(database utils.DatabaseAccessor, session utils.SessionManager, currentUser utils.CurrentUserAccessor) *Authenticator { 18 | return &Authenticator{currentUser, database, session} 19 | } 20 | 21 | func (a *Authenticator) Middleware() negroni.HandlerFunc { 22 | return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { 23 | user := new(models.User) 24 | userID := a.session.Get(r, "UserID") 25 | if bson.IsObjectIdHex(userID) && user.FindByID(bson.ObjectIdHex(userID), a.database.Get(r)) == nil { 26 | a.currentUser.Set(r, user) 27 | } else { 28 | a.currentUser.Clear(r) 29 | } 30 | next(rw, r) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /public/css/_create-service.scss: -------------------------------------------------------------------------------- 1 | .create-service { 2 | .control-label { 3 | font-size: 24px; 4 | } 5 | 6 | .has-error { 7 | .control-label { 8 | color: whitesmoke; 9 | } 10 | 11 | .form-control { 12 | background: #F5D5DE; 13 | } 14 | } 15 | 16 | .form-control-feedback { 17 | color: white; 18 | 19 | margin-right: 15px; 20 | top: 6px; 21 | font-size: 22px; 22 | } 23 | 24 | .has-error .form-control-feedback { 25 | color: #A94442; 26 | } 27 | 28 | .create-service-subtitle { 29 | font-size: 44px; 30 | font-weight: bold; 31 | margin-top: .25em; 32 | } 33 | 34 | .create-service-title { 35 | font-size: 28px; 36 | } 37 | 38 | .create-service-information { 39 | margin-bottom: .75em; 40 | } 41 | 42 | transition: opacity 2s; 43 | opacity: 0; 44 | 45 | &.fade-in { 46 | opacity: 1; 47 | } 48 | 49 | button { 50 | float: left; 51 | margin-top: .5em; 52 | 53 | .glyphicon { 54 | margin-right: .5em; 55 | top: 2px; 56 | } 57 | } 58 | } 59 | 60 | .create-service-home .search-area { 61 | margin-top: 0; 62 | } -------------------------------------------------------------------------------- /public/css/_search.scss: -------------------------------------------------------------------------------- 1 | .search-panel { 2 | background-color: $green; 3 | 4 | p { 5 | font-weight: bold; 6 | } 7 | 8 | img { 9 | padding-top: .5em; 10 | } 11 | } 12 | 13 | .search-home .search-area { 14 | margin-top: 0; 15 | } 16 | 17 | .search-results { 18 | margin-top: 1em; 19 | } 20 | 21 | .search-result { 22 | border: solid 1px; 23 | margin-right: 0.5%; 24 | margin-left: 0.5%; 25 | margin-bottom: 1em; 26 | word-wrap: break-word; 27 | min-height: 120px; 28 | 29 | transition: opacity .2s; 30 | 31 | &.fade-out { 32 | opacity: 0; 33 | } 34 | 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | } 39 | 40 | .search-box { 41 | transition: opacity 1s; 42 | 43 | &.fade-out { 44 | opacity: 0; 45 | } 46 | } 47 | 48 | .more-results { 49 | .glyphicon { 50 | margin-right: .5em; 51 | top: 3px; 52 | } 53 | } 54 | 55 | .search-panel-message { 56 | transition: all 2s; 57 | 58 | &.fadeout { 59 | margin-top: -15px; 60 | font-size: 0; 61 | opacity: 0; 62 | } 63 | } 64 | 65 | .create-search-result { 66 | h2 { 67 | font-weight: bold; 68 | } 69 | 70 | .glyphicon { 71 | margin-top: 30px; 72 | font-size: 60px; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /public/scripts/create-service-home.js: -------------------------------------------------------------------------------- 1 | var ServicePanel = React.createClass({displayName: "ServicePanel", 2 | render: function() { 3 | return ( 4 | React.createElement("div", {className: "search-panel text-center"}, 5 | React.createElement("div", {className: "container"}, 6 | React.createElement(SearchPage, {creating: true, originalTarget: "create-service"}) 7 | ) 8 | ) 9 | ); 10 | } 11 | }); 12 | 13 | var CreateServiceHome = React.createClass({displayName: "CreateServiceHome", 14 | render: function() { 15 | var waitToPop = /^((?!chrome).)*safari/i.test(navigator.userAgent); 16 | $(window).off("popstate").on("popstate", function() { 17 | if (waitToPop) { 18 | waitToPop = false; 19 | return; 20 | } 21 | window.location = window.location.href; 22 | }); 23 | 24 | return ( 25 | React.createElement("div", {className: "create-service-home"}, 26 | React.createElement(Header, {smallTitle: true}), 27 | React.createElement(ServicePanel, null) 28 | ) 29 | ); 30 | } 31 | }); 32 | 33 | React.render( 34 | React.createElement(CreateServiceHome, null), 35 | document.getElementById('content') 36 | ); -------------------------------------------------------------------------------- /public/scripts/service-home.js: -------------------------------------------------------------------------------- 1 | var ServicePanel = React.createClass({displayName: "ServicePanel", 2 | render: function() { 3 | return ( 4 | React.createElement("div", {className: "search-panel text-center"}, 5 | React.createElement("div", {className: "container"}, 6 | React.createElement(SearchPage, {selected: this.props.service}) 7 | ) 8 | ) 9 | ); 10 | } 11 | }); 12 | 13 | var ServiceHome = React.createClass({displayName: "ServiceHome", 14 | render: function() { 15 | var waitToPop = /^((?!chrome).)*safari/i.test(navigator.userAgent); 16 | $(window).off("popstate").on("popstate", function() { 17 | if (waitToPop) { 18 | waitToPop = false; 19 | return; 20 | } 21 | window.location = window.location.href; 22 | }); 23 | 24 | return ( 25 | React.createElement("div", {className: "service-home"}, 26 | React.createElement(Header, {smallTitle: true}), 27 | React.createElement(ServicePanel, {service: JSON.parse($("#content").attr("data-service"))}) 28 | ) 29 | ); 30 | } 31 | }); 32 | 33 | React.render( 34 | React.createElement(ServiceHome, null), 35 | document.getElementById('content') 36 | ); -------------------------------------------------------------------------------- /public/scripts/search-home.js: -------------------------------------------------------------------------------- 1 | var SearchPanel = React.createClass({displayName: "SearchPanel", 2 | render: function() { 3 | return ( 4 | React.createElement("div", {className: "search-panel text-center"}, 5 | React.createElement("div", {className: "container"}, 6 | React.createElement(SearchPage, null) 7 | ) 8 | ) 9 | ); 10 | } 11 | }); 12 | 13 | var SearchHome = React.createClass({displayName: "SearchHome", 14 | componentDidMount: function() { 15 | $(".create-search-result").removeClass("hidden"); 16 | }, 17 | render: function() { 18 | var waitToPop = /^((?!chrome).)*safari/i.test(navigator.userAgent); 19 | $(window).off("popstate").on("popstate", function() { 20 | if (waitToPop) { 21 | waitToPop = false; 22 | return; 23 | } 24 | window.location = window.location.href; 25 | }); 26 | 27 | return ( 28 | React.createElement("div", {className: "search-home"}, 29 | React.createElement(Header, {smallTitle: true}), 30 | React.createElement(SearchPanel, null) 31 | ) 32 | ); 33 | } 34 | }); 35 | 36 | React.render( 37 | React.createElement(SearchHome, null), 38 | document.getElementById('content') 39 | ); -------------------------------------------------------------------------------- /models/analytics.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | "time" 7 | ) 8 | 9 | type Analytics struct { 10 | } 11 | 12 | type deletedUser struct { 13 | *User 14 | DeletedDate time.Time `bson:"deleted_date"` 15 | CodeCount int `bson:"code_count"` 16 | } 17 | 18 | func (a *Analytics) AddDeletedUser(u *User, db *mgo.Database) { 19 | defer db.C("analytics.deleted_user").Insert(deletedUser{u, time.Now(), 0}) 20 | } 21 | 22 | type search struct { 23 | ID bson.ObjectId `bson:"_id"` 24 | Query string `bson:"query"` 25 | Limit int `bson:"limit"` 26 | UserID bson.ObjectId `bson:"user_id,omitempty"` 27 | Date time.Time `bson:"date"` 28 | } 29 | 30 | func (a *Analytics) AddSearch(query string, limit int, userID bson.ObjectId, db *mgo.Database) { 31 | defer db.C("analytics.search").Insert(search{bson.NewObjectId(), query, limit, userID, time.Now()}) 32 | } 33 | 34 | type deletedReferralCode struct { 35 | *ReferralCode 36 | DeletedDate time.Time `bson:"deleted_date"` 37 | } 38 | 39 | func (a *Analytics) AddDeletedReferralCode(c *ReferralCode, db *mgo.Database) { 40 | defer db.C("analytics.deleted_referral_code").Insert(deletedReferralCode{c, time.Now()}) 41 | } 42 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | "time" 7 | ) 8 | 9 | type User struct { 10 | // identification information 11 | ID bson.ObjectId `bson:"_id"` 12 | Email string `bson:"email"` 13 | GoogleToken string `bson:"google_token"` 14 | 15 | // analytics information 16 | SignupDate time.Time `bson:"signup_date"` 17 | LastLoggedIn time.Time `bson:"last_logged_in"` 18 | LoginCount uint `bson:"login_count"` 19 | } 20 | 21 | func NewUser(email, token string) *User { 22 | return &User{ 23 | ID: bson.NewObjectId(), 24 | Email: email, 25 | GoogleToken: token, 26 | SignupDate: time.Now(), 27 | LastLoggedIn: time.Now(), 28 | LoginCount: 1, 29 | } 30 | } 31 | 32 | func (u *User) Update(email, token string, db *mgo.Database) { 33 | u.Email = email 34 | u.GoogleToken = token 35 | u.LastLoggedIn = time.Now() 36 | u.LoginCount++ 37 | u.Save(db) 38 | } 39 | 40 | func (u *User) Save(db *mgo.Database) error { 41 | _, err := u.coll(db).UpsertId(u.ID, u) 42 | return err 43 | } 44 | 45 | func (u *User) FindByEmail(email string, db *mgo.Database) error { 46 | return u.coll(db).Find(bson.M{"email": email}).One(u) 47 | } 48 | 49 | func (u *User) FindByID(id bson.ObjectId, db *mgo.Database) error { 50 | return u.coll(db).FindId(id).One(u) 51 | } 52 | 53 | func (*User) coll(db *mgo.Database) *mgo.Collection { 54 | return db.C("user") 55 | } 56 | 57 | func (u *User) Delete(db *mgo.Database) error { 58 | return u.coll(db).RemoveId(u.ID) 59 | } 60 | -------------------------------------------------------------------------------- /controllers/sitemap_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | "github.com/larryprice/refermadness/utils" 6 | "gopkg.in/mgo.v2/bson" 7 | "net/http" 8 | ) 9 | 10 | type SitemapControllerImpl struct { 11 | database utils.DatabaseAccessor 12 | } 13 | 14 | func NewSitemapController(database utils.DatabaseAccessor) *SitemapControllerImpl { 15 | return &SitemapControllerImpl{ 16 | database: database, 17 | } 18 | } 19 | 20 | func (sc *SitemapControllerImpl) Register(router *mux.Router) { 21 | router.HandleFunc("/sitemap.xml", sc.generate) 22 | } 23 | 24 | type onlyID struct { 25 | ID bson.ObjectId `bson:"_id"` 26 | } 27 | 28 | func (sc *SitemapControllerImpl) generate(w http.ResponseWriter, r *http.Request) { 29 | ids := []onlyID{} 30 | servicesMap := "" 31 | if err := sc.database.Get(r).C("service").Find(nil).All(&ids); err == nil { 32 | for _, id := range ids { 33 | servicesMap += "https://www.refer-madness.com/service/" + id.ID.Hex() +"weekly" 34 | } 35 | } 36 | w.Write([]byte(` 37 | 42 | 43 | https://www.refer-madness.com/ 44 | 45 | 46 | https://www.refer-madness.com/legal 47 | ` + servicesMap + "")) 48 | } 49 | -------------------------------------------------------------------------------- /controllers/create_service_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gorilla/mux" 6 | "github.com/larryprice/refermadness/models" 7 | "github.com/larryprice/refermadness/utils" 8 | "gopkg.in/unrolled/render.v1" 9 | "html/template" 10 | "io/ioutil" 11 | "net/http" 12 | ) 13 | 14 | type CreateServiceControllerImpl struct { 15 | currentUser utils.CurrentUserAccessor 16 | basePage utils.BasePageCreator 17 | renderer *render.Render 18 | database utils.DatabaseAccessor 19 | } 20 | 21 | func NewCreateServiceController(currentUser utils.CurrentUserAccessor, basePage utils.BasePageCreator, 22 | renderer *render.Render, database utils.DatabaseAccessor) *CreateServiceControllerImpl { 23 | return &CreateServiceControllerImpl{ 24 | currentUser: currentUser, 25 | basePage: basePage, 26 | renderer: renderer, 27 | database: database, 28 | } 29 | } 30 | 31 | func (sc *CreateServiceControllerImpl) Register(router *mux.Router) { 32 | router.HandleFunc("/service/create", sc.view).Methods("GET") 33 | router.HandleFunc("/service/create", sc.create).Methods("POST") 34 | } 35 | 36 | func (sc *CreateServiceControllerImpl) view(w http.ResponseWriter, r *http.Request) { 37 | t, _ := template.ParseFiles("views/layout.html", "views/create-service.html") 38 | t.Execute(w, sc.basePage.Get(r)) 39 | } 40 | 41 | func (sc *CreateServiceControllerImpl) create(w http.ResponseWriter, r *http.Request) { 42 | var serviceData map[string]string 43 | body, _ := ioutil.ReadAll(r.Body) 44 | if err := json.Unmarshal(body, &serviceData); err != nil { 45 | sc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 46 | "error": err.Error(), 47 | }) 48 | return 49 | } 50 | 51 | if serviceData["name"] == "" || serviceData["description"] == "" || serviceData["url"] == "" { 52 | sc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 53 | "error": "All fields must be filled out.", 54 | }) 55 | return 56 | } 57 | 58 | service := models.NewService(serviceData["name"], serviceData["description"], serviceData["url"], sc.currentUser.Get(r).ID) 59 | service.Save(sc.database.Get(r)) 60 | sc.renderer.JSON(w, http.StatusCreated, service) 61 | } 62 | -------------------------------------------------------------------------------- /public/css/_account.scss: -------------------------------------------------------------------------------- 1 | .account-home { 2 | .login-settings { 3 | h2 { 4 | margin-top: 1em; 5 | } 6 | 7 | button { 8 | white-space: normal; 9 | 10 | .glyphicon { 11 | margin-right: .5em; 12 | top: 3px; 13 | } 14 | 15 | &.delete-account { 16 | margin-top: 2em; 17 | margin-bottom: 3em; 18 | 19 | transition: opacity .5s, margin-top .3s .2s, height .5s .1s; 20 | opacity: 1; 21 | 22 | &.fade-out { 23 | opacity: 0; 24 | height: 0; 25 | margin-top: -15px; 26 | } 27 | } 28 | } 29 | 30 | .switch-accounts-cancel { 31 | margin-left: .5em; 32 | } 33 | 34 | .switch-account-information { 35 | margin-top: 2em; 36 | 37 | transition: opacity .5s; 38 | opacity: 1; 39 | 40 | &.fade-out { 41 | opacity: 0; 42 | } 43 | 44 | .switch-account-confirmation { 45 | font-size: 24px; 46 | margin-right: 1em; 47 | } 48 | 49 | .google-plus { 50 | background-image: url("/img/google-plus.png"); 51 | background-repeat: no-repeat; 52 | 53 | background-size: 18px 18px; 54 | 55 | width: 18px; 56 | height: 18px; 57 | top: .2em; 58 | margin-right: .5em; 59 | } 60 | } 61 | 62 | .desperate-delete-message, .apologetic-delete-message, .warning-delete-message { 63 | margin-bottom: 1em; 64 | 65 | button { 66 | margin-top: 1em; 67 | 68 | &.disabled { 69 | opacity: .9; 70 | pointer-events: auto; 71 | } 72 | } 73 | 74 | &.collapsing { 75 | max-height: 230px; 76 | } 77 | 78 | .cancel-account-deletion { 79 | margin-left: 1em; 80 | } 81 | } 82 | 83 | .warning-delete-message { 84 | form { 85 | margin-bottom: 0; 86 | } 87 | 88 | .form-group { 89 | margin-top: .5em; 90 | margin-bottom: .5em; 91 | } 92 | } 93 | } 94 | 95 | .user-referral-codes { 96 | border-top: .1em dotted whitesmoke; 97 | padding-top: 1.5em; 98 | 99 | > h2 { 100 | margin-bottom: 1.5em; 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /public/css/_landing.scss: -------------------------------------------------------------------------------- 1 | #no-family, #no-friends, #no-followers { 2 | width: 100%; 3 | height: 150px; 4 | background-size: 150px; 5 | padding: 3em; 6 | 7 | &:after { 8 | background: 9 | linear-gradient(-135deg, 10 | rgba(0,0,0,0) 0%, 11 | rgba(0,0,0,0) calc(50% - 2px), 12 | whitesmoke 50%, 13 | rgba(0,0,0,0) calc(50% + 2px), 14 | rgba(0,0,0,0) 100%), 15 | linear-gradient(-45deg, 16 | rgba(0,0,0,0) 0%, 17 | rgba(0,0,0,0) calc(50% - 2px), 18 | whitesmoke 50%, 19 | rgba(0,0,0,0) calc(50% + 2px), 20 | rgba(0,0,0,0) 100%); 21 | width: 100%; 22 | height: 100%; 23 | position: absolute; 24 | bottom: 0; 25 | left: 0; 26 | content: ''; 27 | } 28 | } 29 | 30 | #no-friends { 31 | background: url("/img/no-friends.png") no-repeat scroll 50% 50%; 32 | } 33 | 34 | #no-family { 35 | background: url("/img/no-family.png") no-repeat scroll 50% 50%; 36 | } 37 | 38 | #no-followers { 39 | background: url("/img/no-followers.png") no-repeat scroll 50% 50%; 40 | } 41 | 42 | #no-problem { 43 | margin-top: 2em; 44 | margin-bottom: 2em; 45 | 46 | h1 { 47 | font-size: 48px; 48 | font-weight: bold; 49 | margin-top: 1em; 50 | } 51 | } 52 | 53 | .get-started-panel { 54 | .btn-default { 55 | font-size: 36px; 56 | background-color: transparent; 57 | color: inherit; 58 | border-color: whitesmoke; 59 | margin-top: .5em; 60 | margin-bottom: 2em; 61 | white-space: normal; 62 | } 63 | 64 | .glyphicon-search { 65 | top: 5px; 66 | margin-right: .4em; 67 | } 68 | } 69 | 70 | .recent-panel .container { 71 | border-top: solid whitesmoke; 72 | } 73 | 74 | .lonely-panel, 75 | .recent-panel, 76 | .popular-panel { 77 | background-color: $light-green; 78 | } 79 | 80 | .hook-panel, 81 | .get-started-panel { 82 | background-color: $lightest-green; 83 | } 84 | 85 | .popular-panel, .recent-panel { 86 | .row { 87 | padding-top: 1em; 88 | padding-bottom: .6em; 89 | } 90 | 91 | .search-result { 92 | margin-top: .4em; 93 | } 94 | } 95 | 96 | .service-area { 97 | margin-top: 1.5em; 98 | } 99 | 100 | .search-area { 101 | padding: 1em; 102 | margin-top: 20px; 103 | } -------------------------------------------------------------------------------- /public/css/_layout.scss: -------------------------------------------------------------------------------- 1 | #content { 2 | font-size: 28px; 3 | font-family: 'Muli', sans-serif; 4 | background-color: $green; 5 | color: whitesmoke; 6 | } 7 | 8 | .title { 9 | margin-top: 0; 10 | padding-top: 0.2em; 11 | font-family: 'Monoton', cursive; 12 | font-size: 56px; 13 | transition: font-size 2s; 14 | 15 | &.fast { 16 | transition: font-size 1s; 17 | } 18 | 19 | a { 20 | color: white; 21 | text-decoration: none; 22 | } 23 | 24 | &.shrink { 25 | font-size: 48px; 26 | } 27 | } 28 | 29 | .login-btn, .account-btn, .logout-btn { 30 | margin-top: 1em; 31 | 32 | .glyphicon { 33 | font-size: 12px; 34 | margin-right: .5em; 35 | } 36 | } 37 | 38 | .account-btn { 39 | margin-right: .5em; 40 | } 41 | 42 | #authenticate-panel { 43 | background-color: $lightest-green; 44 | 45 | .glyphicon-question-sign { 46 | font-size: 14px; 47 | vertical-align: top; 48 | cursor: pointer; 49 | } 50 | 51 | button { 52 | font-size: 24px; 53 | margin-top: 1.5em; 54 | margin-bottom: .5em; 55 | 56 | .google-plus { 57 | background-image: url("/img/google-plus.png"); 58 | background-repeat: no-repeat; 59 | 60 | background-size: 24px 24px; 61 | 62 | width: 24px; 63 | height: 24px; 64 | top: .2em; 65 | margin-right: .5em; 66 | } 67 | } 68 | 69 | #login-faq { 70 | .login-faq-question, .login-faq-answer { 71 | font-size: 16px; 72 | text-align: left; 73 | } 74 | 75 | .login-faq-answer { 76 | margin-left: 1em; 77 | } 78 | } 79 | } 80 | 81 | @media (min-width: 380px) { 82 | .title { 83 | font-size: 64px; 84 | } 85 | } 86 | 87 | @media (min-width: 768px) { 88 | .title { 89 | font-size: 108px; 90 | } 91 | } 92 | 93 | html, body { 94 | font-family: 'Muli', sans-serif; 95 | background-color: $green; 96 | color: whitesmoke; 97 | } 98 | 99 | html { 100 | position: relative; 101 | min-height: 100%; 102 | } 103 | 104 | body { 105 | margin-bottom: 50px; 106 | } 107 | 108 | #footer { 109 | position: absolute; 110 | left: 0; 111 | bottom: 0; 112 | height: 50px; 113 | width: 100%; 114 | color: whitesmoke; 115 | background-color: $green; 116 | 117 | small { 118 | font-size : 14px; 119 | } 120 | 121 | .container { 122 | border-top: .1em solid whitesmoke; 123 | padding: 15px; 124 | padding-bottom: 0; 125 | } 126 | 127 | a { 128 | color: whitesmoke; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /models/service.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Service struct { 11 | // identification information 12 | ID bson.ObjectId `bson:"_id"` 13 | Name string `bson:"name"` 14 | Description string `bson:"description"` 15 | URL string `bson:"url"` 16 | Search string `bson:"search"` 17 | 18 | // analytics information 19 | CreatedDate time.Time `bson:"created_date"` 20 | LastSelected time.Time `bson:"last_selected"` 21 | SelectedCount uint `bson:"selected_count"` 22 | CreatedBy bson.ObjectId `bson:"created_by"` 23 | } 24 | 25 | func NewService(name, description, url string, creatorID bson.ObjectId) *Service { 26 | url = strings.TrimPrefix(strings.TrimPrefix(url, "http://"), "https://") 27 | return &Service{ 28 | ID: bson.NewObjectId(), 29 | Name: name, 30 | URL: url, 31 | Description: description, 32 | CreatedDate: time.Now(), 33 | LastSelected: time.Now(), 34 | SelectedCount: 1, 35 | CreatedBy: creatorID, 36 | Search: strings.ToLower(name) + ";" + strings.ToLower(description) + ";" + strings.ToLower(url), 37 | } 38 | } 39 | 40 | func (s *Service) Save(db *mgo.Database) error { 41 | _, err := s.coll(db).UpsertId(s.ID, s) 42 | return err 43 | } 44 | 45 | func (s *Service) FindByID(id bson.ObjectId, db *mgo.Database) error { 46 | return s.coll(db).FindId(id).One(s) 47 | } 48 | 49 | func (s *Service) WasSelected(db *mgo.Database) error { 50 | s.SelectedCount++ 51 | s.LastSelected = time.Now() 52 | return s.Save(db) 53 | } 54 | 55 | func (*Service) coll(db *mgo.Database) *mgo.Collection { 56 | return db.C("service") 57 | } 58 | 59 | type Services []Service 60 | 61 | func (s *Services) FindRelevant(query string, limit, skip int, db *mgo.Database) (int, error) { 62 | q := s.coll(db).Find(bson.M{"search": &bson.RegEx{Pattern: strings.ToLower(query)}}) 63 | total, _ := q.Count() 64 | return total, q.Skip(skip).Limit(limit).All(s) 65 | } 66 | 67 | func (s *Services) FindByIDs(ids []bson.ObjectId, db *mgo.Database) error { 68 | return s.coll(db).Find(bson.M{"_id": bson.M{"$in": ids}}).Sort("name").All(s) 69 | } 70 | 71 | func (s *Services) FindMostPopular(limit int, db *mgo.Database) error { 72 | return s.coll(db).Find(nil).Sort("-selected_count").Limit(limit).All(s) 73 | } 74 | 75 | func (s *Services) FindMostRecent(limit int, db *mgo.Database) error { 76 | return s.coll(db).Find(nil).Sort("-last_selected").Limit(limit).All(s) 77 | } 78 | 79 | func (*Services) coll(db *mgo.Database) *mgo.Collection { 80 | return db.C("service") 81 | } 82 | -------------------------------------------------------------------------------- /public/css/_common.scss: -------------------------------------------------------------------------------- 1 | $green: #00B655; 2 | $light-green: #2ABB6D; 3 | $lightest-green: #4EC987; 4 | $dark-green: #008D42; 5 | $darkest-green: #006F34; 6 | 7 | @font-face { 8 | font-family: 'Muli'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Muli'), url(fonts/Muli.woff2) format('woff2'), url(fonts/Muli.woff) format('woff'); 12 | } 13 | 14 | @font-face { 15 | font-family: 'Monoton'; 16 | font-style: normal; 17 | font-weight: 400; 18 | src: local('Monoton'), local('Monoton-Regular'), url(fonts/Monoton.woff2) format('woff2'), url(fonts/Monoton.woff) format('woff'); 19 | } 20 | 21 | .btn-default, 22 | .btn-default:hover, 23 | .btn-default:focus { 24 | background-color: transparent; 25 | color: inherit; 26 | border-color: whitesmoke; 27 | white-space: normal; 28 | } 29 | 30 | .btn-google, 31 | .btn-google:hover, 32 | .btn-google:focus { 33 | background-color: #2196f3; 34 | } 35 | 36 | .btn-default:hover { 37 | opacity: 0.85; 38 | } 39 | 40 | a, .btn-link { 41 | color: lavender; 42 | 43 | &:hover { 44 | color: lavender; 45 | } 46 | } 47 | 48 | input[type=text].disabled { 49 | color: grey; 50 | font-style: italic; 51 | cursor: pointer; 52 | } 53 | 54 | @media (min-width: 768px) { 55 | .col-md-3-point-5 { 56 | width: 49%; 57 | } 58 | } 59 | 60 | @media (min-width: 992px) { 61 | .col-md-3-point-5 { 62 | width: 24%; 63 | } 64 | } 65 | 66 | .glyphicon { 67 | &.spin { 68 | animation-name: loading-spin; 69 | animation-duration: 1s; 70 | animation-timing-function: linear; 71 | -webkit-animation-name: loading-spin; 72 | -webkit-animation-duration: 1s; 73 | -webkit-animation-timing-function: linear; 74 | 75 | &.infinite { 76 | animation-iteration-count: infinite; 77 | -webkit-animation-iteration-count: infinite; 78 | } 79 | 80 | &.fast { 81 | animation-duration: 350ms; 82 | -webkit-animation-duration: 350ms; 83 | } 84 | } 85 | 86 | &.shake { 87 | animation-name: shake; 88 | animation-duration: 200ms; 89 | animation-iteration-count: 2; 90 | animation-timing-function: linear; 91 | transform-origin: 50% 50%; 92 | 93 | -webkit-animation-name: shake; 94 | -webkit-animation-duration: 200ms; 95 | -webkit-animation-iteration-count: 2; 96 | -webkit-animation-timing-function: linear; 97 | } 98 | } 99 | 100 | @keyframes loading-spin { 101 | from { 102 | transform: rotate(0deg); 103 | } 104 | to { 105 | transform: rotate(360deg); 106 | } 107 | } 108 | 109 | 110 | @-webkit-keyframes loading-spin { 111 | from { 112 | -webkit-transform: rotate(0deg); 113 | } 114 | to { 115 | -webkit-transform: rotate(360deg); 116 | } 117 | } 118 | 119 | #terms { 120 | p { 121 | font-size: 14px; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /web/server.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/codegangsta/negroni" 5 | "github.com/goincremental/negroni-sessions" 6 | "github.com/goincremental/negroni-sessions/cookiestore" 7 | "github.com/gorilla/mux" 8 | "github.com/larryprice/refermadness/controllers" 9 | "github.com/larryprice/refermadness/utils" 10 | "github.com/larryprice/refermadness/web/middleware" 11 | "github.com/unrolled/secure" 12 | "gopkg.in/unrolled/render.v1" 13 | "html/template" 14 | "net/http" 15 | ) 16 | 17 | type Server struct { 18 | *negroni.Negroni 19 | } 20 | 21 | func NewServer(dba utils.DatabaseAccessor, cua utils.CurrentUserAccessor, clientID, clientSecret, 22 | sessionSecret string, isDevelopment bool, gaKey string) *Server { 23 | s := Server{negroni.Classic()} 24 | session := utils.NewSessionManager() 25 | basePage := utils.NewBasePageCreator(cua, gaKey) 26 | renderer := render.New() 27 | 28 | router := mux.NewRouter() 29 | router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 30 | t, _ := template.ParseFiles("views/layout.html", "views/index.html") 31 | t.Execute(w, basePage.Get(r)) 32 | }) 33 | router.HandleFunc("/legal", func(w http.ResponseWriter, r *http.Request) { 34 | t, _ := template.ParseFiles("views/layout.html", "views/legal.html") 35 | t.Execute(w, basePage.Get(r)) 36 | }) 37 | router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 | t, _ := template.ParseFiles("views/layout.html", "views/404.html") 39 | t.Execute(w, basePage.Get(r)) 40 | }) 41 | 42 | accountController := controllers.NewAccountController(clientID, clientSecret, isDevelopment, session, dba, cua, basePage, renderer) 43 | accountController.Register(router) 44 | createServiceController := controllers.NewCreateServiceController(cua, basePage, renderer, dba) 45 | createServiceController.Register(router) 46 | serviceController := controllers.NewServiceController(cua, basePage, renderer, dba) 47 | serviceController.Register(router) 48 | codeController := controllers.NewReferralCodeController(cua, renderer, dba) 49 | codeController.Register(router) 50 | searchController := controllers.NewSearchController(cua, basePage, renderer, dba) 51 | searchController.Register(router) 52 | sitemapController := controllers.NewSitemapController(dba) 53 | sitemapController.Register(router) 54 | 55 | s.Use(negroni.HandlerFunc(secure.New(secure.Options{ 56 | AllowedHosts: []string{"www.refer-madness.com", "refer-madness.com"}, 57 | ContentTypeNosniff: true, 58 | BrowserXssFilter: true, 59 | FrameDeny: true, 60 | IsDevelopment: isDevelopment, 61 | }).HandlerFuncWithNext)) 62 | s.Use(sessions.Sessions("refermadness", cookiestore.New([]byte(sessionSecret)))) 63 | s.Use(middleware.NewDatabase(dba).Middleware()) 64 | s.Use(middleware.NewAuthenticator(dba, session, cua).Middleware()) 65 | s.UseHandler(router) 66 | 67 | return &s 68 | } 69 | -------------------------------------------------------------------------------- /controllers/search_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/gorilla/mux" 7 | "github.com/larryprice/refermadness/models" 8 | "github.com/larryprice/refermadness/utils" 9 | "gopkg.in/mgo.v2/bson" 10 | "gopkg.in/unrolled/render.v1" 11 | "html/template" 12 | "net/http" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | type SearchControllerImpl struct { 18 | currentUser utils.CurrentUserAccessor 19 | basePage utils.BasePageCreator 20 | renderer *render.Render 21 | database utils.DatabaseAccessor 22 | } 23 | 24 | func NewSearchController(currentUser utils.CurrentUserAccessor, basePage utils.BasePageCreator, 25 | renderer *render.Render, database utils.DatabaseAccessor) *SearchControllerImpl { 26 | return &SearchControllerImpl{ 27 | currentUser: currentUser, 28 | basePage: basePage, 29 | renderer: renderer, 30 | database: database, 31 | } 32 | } 33 | 34 | func (sc *SearchControllerImpl) Register(router *mux.Router) { 35 | router.HandleFunc("/search", sc.search) 36 | } 37 | 38 | type searchPage struct { 39 | utils.BasePage 40 | ResultString string 41 | } 42 | 43 | func (sc *SearchControllerImpl) search(w http.ResponseWriter, r *http.Request) { 44 | data, err := sc.get(w, r) 45 | 46 | if len(r.Header["Content-Type"]) == 1 && strings.Contains(r.Header["Content-Type"][0], "application/json") { 47 | if err != nil { 48 | sc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 49 | "error": err.Error(), 50 | }) 51 | return 52 | } 53 | sc.renderer.JSON(w, http.StatusOK, data) 54 | return 55 | } 56 | 57 | resultString, _ := json.Marshal(data) 58 | t, _ := template.ParseFiles("views/layout.html", "views/search.html") 59 | t.Execute(w, searchPage{sc.basePage.Get(r), string(resultString)}) 60 | } 61 | 62 | type searchResult struct { 63 | *models.Services 64 | Total int 65 | } 66 | 67 | func (sc *SearchControllerImpl) get(w http.ResponseWriter, r *http.Request) (searchResult, error) { 68 | services := new(models.Services) 69 | query := r.FormValue("q") 70 | if query == "" { 71 | return searchResult{services, 0}, nil 72 | } 73 | 74 | var limit int 75 | var err error 76 | 77 | if limit, err = strconv.Atoi(r.FormValue("limit")); err != nil { 78 | limit = 11 79 | } 80 | 81 | if limit > 50 { 82 | limit = 50 83 | } 84 | 85 | var skip int 86 | if skip, err = strconv.Atoi(r.FormValue("skip")); err != nil { 87 | skip = 0 88 | } 89 | 90 | var total int 91 | db := sc.database.Get(r) 92 | if total, err = services.FindRelevant(query, limit, skip, db); err != nil { 93 | return searchResult{}, errors.New("Database error: " + err.Error()) 94 | } 95 | 96 | var userID bson.ObjectId 97 | if user := sc.currentUser.Get(r); user != nil { 98 | userID = user.ID 99 | } 100 | 101 | analytics := new(models.Analytics) 102 | analytics.AddSearch(query, limit, userID, db) 103 | 104 | return searchResult{services, total}, nil 105 | } 106 | -------------------------------------------------------------------------------- /models/referral_code.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | type ReferralCode struct { 11 | ID bson.ObjectId `bson:"_id"` 12 | UserID bson.ObjectId `bson:"user_id"` 13 | ServiceID bson.ObjectId `bson:"service_id"` 14 | Code string `bson:"code"` 15 | 16 | // Analytics 17 | DateAdded time.Time `bson:"date_added"` 18 | DateUpdated time.Time `bson:"date_updated"` 19 | DateLastViewed time.Time `bson:"date_last_viewed"` 20 | Views uint `bson:"total_views"` 21 | ViewsSinceUpdate uint `bson:"views"` 22 | Edits uint `bson:"edits"` 23 | Flags uint `bson:"flags"` 24 | } 25 | 26 | func NewReferralCode(code string, userID, serviceID bson.ObjectId) *ReferralCode { 27 | return &ReferralCode{ 28 | ID: bson.NewObjectId(), 29 | UserID: userID, 30 | ServiceID: serviceID, 31 | Code: code, 32 | DateAdded: time.Now(), 33 | DateUpdated: time.Now(), 34 | DateLastViewed: time.Now(), 35 | Views: 0, 36 | ViewsSinceUpdate: 0, 37 | Edits: 0, 38 | Flags: 0, 39 | } 40 | } 41 | 42 | func (c *ReferralCode) Save(db *mgo.Database) error { 43 | _, err := c.coll(db).UpsertId(c.ID, c) 44 | return err 45 | } 46 | 47 | func (c *ReferralCode) Edit(code string, db *mgo.Database) error { 48 | c.Code = code 49 | c.Edits++ 50 | c.ViewsSinceUpdate = 0 51 | c.DateUpdated = time.Now() 52 | return c.Save(db) 53 | } 54 | 55 | func (c *ReferralCode) Delete(db *mgo.Database) error { 56 | return c.coll(db).RemoveId(c.ID) 57 | } 58 | 59 | func (c *ReferralCode) FindByUserAndService(userID, serviceID bson.ObjectId, db *mgo.Database) error { 60 | return c.coll(db).Find(bson.M{"user_id": userID, "service_id": serviceID}).One(&c) 61 | } 62 | 63 | func (c *ReferralCode) FindByID(id bson.ObjectId, db *mgo.Database) error { 64 | return c.coll(db).FindId(id).One(&c) 65 | } 66 | 67 | func (c *ReferralCode) WasViewed(db *mgo.Database) error { 68 | c.Views++ 69 | c.ViewsSinceUpdate++ 70 | return c.Save(db) 71 | } 72 | 73 | func (c *ReferralCode) WasReported(userID bson.ObjectId, db *mgo.Database) error { 74 | c.Flags++ 75 | return c.Save(db) 76 | } 77 | 78 | func (c *ReferralCode) FindRandom(serviceID bson.ObjectId, db *mgo.Database) error { 79 | q := c.coll(db).Find(bson.M{"service_id": serviceID}) 80 | count, _ := q.Count() 81 | if count > 0 { 82 | return q.Skip(rand.Intn(count)).Limit(1).One(c) 83 | } 84 | return nil 85 | } 86 | 87 | func (*ReferralCode) coll(db *mgo.Database) *mgo.Collection { 88 | return db.C("referral_code") 89 | } 90 | 91 | type ReferralCodes []ReferralCode 92 | 93 | func (c *ReferralCodes) FindByUserID(userID bson.ObjectId, limit, skip int, db *mgo.Database) (int, error) { 94 | q := c.coll(db).Find(bson.M{"user_id": userID}) 95 | total, _ := q.Count() 96 | return total, q.Skip(skip).Limit(limit).All(c) 97 | } 98 | 99 | func (*ReferralCodes) coll(db *mgo.Database) *mgo.Collection { 100 | return db.C("referral_code") 101 | } 102 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Refer Madness 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 33 | 34 | 35 | {{template "body" .}} 36 | 37 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /public/css/_service.scss: -------------------------------------------------------------------------------- 1 | .search-area { 2 | margin-top: 0; 3 | 4 | .service-name { 5 | font-size: 44px; 6 | font-weight: bold; 7 | } 8 | 9 | .random-referral-code { 10 | margin-top: 2em; 11 | 12 | .referral-code-actions { 13 | margin-top: 1.5em; 14 | transition: opacity .5s; 15 | 16 | button { 17 | margin-right: 1em; 18 | margin-left: 1em; 19 | } 20 | 21 | &.fade-in { 22 | opacity: 1; 23 | } 24 | 25 | &.fade-out { 26 | opacity: 0; 27 | } 28 | 29 | .glyphicon { 30 | margin-right: .5em; 31 | } 32 | 33 | .report-bad-code { 34 | display: inline; 35 | 36 | > span.report-code-ask { 37 | font-size: 15px; 38 | vertical-align: middle; 39 | } 40 | 41 | .report-code-cancel { 42 | margin-left: 0; 43 | } 44 | } 45 | } 46 | } 47 | 48 | .referral-code { 49 | transition: opacity .4s; 50 | 51 | &.fade-in { 52 | opacity: 1; 53 | } 54 | 55 | &.fade-out { 56 | opacity: 0; 57 | } 58 | } 59 | 60 | .add-referral-code, .edit-referral-code, .referral-code-actions { 61 | } 62 | 63 | .view-result a { 64 | word-wrap: break-word; 65 | } 66 | 67 | .add-referral-code { 68 | .btn-default span { 69 | margin-right: .5em; 70 | } 71 | 72 | .add-code-btn { 73 | .glyphicon { 74 | top: -3px; 75 | } 76 | } 77 | 78 | .add-code-msg { 79 | display: inline-block; 80 | width: 14em; 81 | overflow: hidden; 82 | white-space: nowrap; 83 | transition: width 2s; 84 | 85 | &.hide-me { 86 | width: 0; 87 | overflow: hidden; 88 | margin-right: -9px !important; 89 | white-space: nowrap; 90 | } 91 | } 92 | } 93 | 94 | .edit-referral-code .referral-code-views { 95 | margin-top: .5em; 96 | font-size: 60%; 97 | } 98 | 99 | .add-referral-code, .edit-referral-code { 100 | margin-top: 5em; 101 | 102 | .add-code-btn { 103 | margin-top: 3px; 104 | height: 46px; 105 | 106 | &.hide-me { 107 | border-top-left-radius: 0; 108 | border-bottom-left-radius: 0; 109 | border-left-color: lightgrey; 110 | } 111 | 112 | .glyphicon { 113 | transition: opacity .75s; 114 | 115 | &.fade-in { 116 | opacity: 1; 117 | } 118 | 119 | &.fade-out { 120 | opacity: 0; 121 | } 122 | } 123 | } 124 | 125 | .add-code-entry { 126 | display: inline-block; 127 | width: 18em; 128 | transition: width 2s, opacity 2s, padding 2s; 129 | border-top-right-radius: 0; 130 | border-bottom-right-radius: 0; 131 | 132 | &.hide-me { 133 | opacity: 0; 134 | width: 0; 135 | padding: 0; 136 | } 137 | } 138 | 139 | @media (max-width: 400px) { 140 | .add-code-entry:not(.hide-me) { 141 | width: 12em; 142 | } 143 | 144 | .add-code-btn:not(.hide-me) { 145 | font-size: 15px; 146 | } 147 | } 148 | } 149 | } 150 | 151 | @keyframes shake { 152 | 0% { transform: scale(1); } 153 | 50% {transform: scale(0.7); } 154 | 100% { transform: scale(1); } 155 | } 156 | 157 | @-webkit-keyframes shake { 158 | 0% { transform: scale(1); } 159 | 50% {transform: scale(0.7); } 160 | 100% { transform: scale(1); } 161 | } 162 | -------------------------------------------------------------------------------- /controllers/service_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/gorilla/mux" 7 | "github.com/larryprice/refermadness/models" 8 | "github.com/larryprice/refermadness/utils" 9 | "gopkg.in/mgo.v2/bson" 10 | "gopkg.in/unrolled/render.v1" 11 | "html/template" 12 | "net/http" 13 | "strings" 14 | ) 15 | 16 | type ServiceControllerImpl struct { 17 | currentUser utils.CurrentUserAccessor 18 | basePage utils.BasePageCreator 19 | renderer *render.Render 20 | database utils.DatabaseAccessor 21 | } 22 | 23 | func NewServiceController(currentUser utils.CurrentUserAccessor, basePage utils.BasePageCreator, 24 | renderer *render.Render, database utils.DatabaseAccessor) *ServiceControllerImpl { 25 | return &ServiceControllerImpl{ 26 | currentUser: currentUser, 27 | basePage: basePage, 28 | renderer: renderer, 29 | database: database, 30 | } 31 | } 32 | 33 | func (sc *ServiceControllerImpl) Register(router *mux.Router) { 34 | router.HandleFunc("/service/popular", sc.popular) 35 | router.HandleFunc("/service/recent", sc.recent) 36 | router.HandleFunc("/service/{id}", sc.single) 37 | } 38 | 39 | func (sc *ServiceControllerImpl) popular(w http.ResponseWriter, r *http.Request) { 40 | services := new(models.Services) 41 | if err := services.FindMostPopular(3, sc.database.Get(r)); err != nil { 42 | sc.renderer.JSON(w, http.StatusInternalServerError, map[string]string{ 43 | "error": err.Error(), 44 | }) 45 | return 46 | } 47 | 48 | sc.renderer.JSON(w, http.StatusOK, services) 49 | } 50 | 51 | func (sc *ServiceControllerImpl) recent(w http.ResponseWriter, r *http.Request) { 52 | services := new(models.Services) 53 | if err := services.FindMostRecent(3, sc.database.Get(r)); err != nil { 54 | sc.renderer.JSON(w, http.StatusInternalServerError, map[string]string{ 55 | "error": err.Error(), 56 | }) 57 | return 58 | } 59 | 60 | sc.renderer.JSON(w, http.StatusOK, services) 61 | } 62 | 63 | type serviceResult struct { 64 | *models.Service 65 | RandomCode *models.ReferralCode 66 | UserCode *models.ReferralCode 67 | } 68 | 69 | type servicePage struct { 70 | utils.BasePage 71 | ResultString string 72 | } 73 | 74 | func (sc *ServiceControllerImpl) single(w http.ResponseWriter, r *http.Request) { 75 | data, err := sc.get(w, r) 76 | 77 | if len(r.Header["Content-Type"]) == 1 && strings.Contains(r.Header["Content-Type"][0], "application/json") { 78 | if err != nil { 79 | sc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 80 | "error": err.Error(), 81 | }) 82 | return 83 | } 84 | sc.renderer.JSON(w, http.StatusOK, data) 85 | return 86 | } else if err != nil { 87 | http.Error(w, err.Error(), http.StatusBadRequest) 88 | } 89 | 90 | resultString, _ := json.Marshal(data) 91 | t, _ := template.ParseFiles("views/layout.html", "views/service.html") 92 | t.Execute(w, servicePage{sc.basePage.Get(r), string(resultString)}) 93 | } 94 | 95 | func (sc *ServiceControllerImpl) get(w http.ResponseWriter, r *http.Request) (serviceResult, error) { 96 | if !bson.IsObjectIdHex(mux.Vars(r)["id"]) { 97 | return serviceResult{}, errors.New("Not a valid ID.") 98 | } 99 | service := new(models.Service) 100 | db := sc.database.Get(r) 101 | if err := service.FindByID(bson.ObjectIdHex(mux.Vars(r)["id"]), db); !service.ID.Valid() || err != nil { 102 | return serviceResult{}, errors.New("No such service.") 103 | } 104 | defer service.WasSelected(db) 105 | 106 | refCode := new(models.ReferralCode) 107 | if err := refCode.FindRandom(service.ID, db); err != nil { 108 | return serviceResult{}, errors.New("Internal error.") 109 | } else { 110 | defer refCode.WasViewed(db) 111 | } 112 | 113 | userRefCode := new(models.ReferralCode) 114 | if user := sc.currentUser.Get(r); user != nil { 115 | userRefCode.FindByUserAndService(sc.currentUser.Get(r).ID, service.ID, db) 116 | } 117 | 118 | return serviceResult{service, refCode, userRefCode}, nil 119 | } 120 | -------------------------------------------------------------------------------- /public/scripts/noty-theme.js: -------------------------------------------------------------------------------- 1 | $.noty.themes.refermadness = { 2 | name: 'refermadness', 3 | helpers: {}, 4 | modal: { 5 | css: { 6 | position: 'fixed', 7 | width: '100%', 8 | height: '100%', 9 | backgroundColor: '#000', 10 | zIndex: 10000, 11 | opacity: 0.6, 12 | display: 'none', 13 | left: 0, 14 | top: 0 15 | } 16 | }, 17 | style: function () { 18 | 19 | this.$bar.css({ 20 | overflow: 'hidden', 21 | margin: '4px 0', 22 | borderRadius: '2px' 23 | }); 24 | 25 | this.$message.css({ 26 | fontSize: '14px', 27 | lineHeight: '16px', 28 | textAlign: 'center', 29 | padding: '10px', 30 | width: 'auto', 31 | position: 'relative' 32 | }); 33 | 34 | this.$closeButton.css({ 35 | position: 'absolute', 36 | top: 4, 37 | right: 4, 38 | width: 10, 39 | height: 10, 40 | background: "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAxUlEQVR4AR3MPUoDURSA0e++uSkkOxC3IAOWNtaCIDaChfgXBMEZbQRByxCwk+BasgQRZLSYoLgDQbARxry8nyumPcVRKDfd0Aa8AsgDv1zp6pYd5jWOwhvebRTbzNNEw5BSsIpsj/kurQBnmk7sIFcCF5yyZPDRG6trQhujXYosaFoc+2f1MJ89uc76IND6F9BvlXUdpb6xwD2+4q3me3bysiHvtLYrUJto7PD/ve7LNHxSg/woN2kSz4txasBdhyiz3ugPGetTjm3XRokAAAAASUVORK5CYII=)", 41 | display: 'none', 42 | cursor: 'pointer' 43 | }); 44 | 45 | this.$buttons.css({ 46 | padding: 5, 47 | textAlign: 'right', 48 | borderTop: '1px solid #ccc', 49 | backgroundColor: '#fff' 50 | }); 51 | 52 | this.$buttons.find('button').css({ 53 | marginLeft: 5 54 | }); 55 | 56 | this.$buttons.find('button:first').css({ 57 | marginLeft: 0 58 | }); 59 | 60 | this.$bar.on({ 61 | mouseenter: function () { 62 | $(this).find('.noty_close').stop().fadeTo('normal', 1); 63 | }, 64 | mouseleave: function () { 65 | $(this).find('.noty_close').stop().fadeTo('normal', 0); 66 | } 67 | }); 68 | 69 | switch (this.options.layout.name) { 70 | case 'top': 71 | this.$bar.css({ 72 | borderBottom: '2px solid #eee', 73 | borderLeft: '2px solid #eee', 74 | borderRight: '2px solid #eee', 75 | borderTop: '2px solid #eee', 76 | boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)" 77 | }); 78 | break; 79 | case 'topCenter': 80 | case 'center': 81 | case 'bottomCenter': 82 | case 'inline': 83 | this.$bar.css({ 84 | border: '1px solid #eee', 85 | boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)" 86 | }); 87 | this.$message.css({ 88 | fontSize: '13px', 89 | textAlign: 'center' 90 | }); 91 | break; 92 | case 'topLeft': 93 | case 'topRight': 94 | case 'bottomLeft': 95 | case 'bottomRight': 96 | case 'centerLeft': 97 | case 'centerRight': 98 | this.$bar.css({ 99 | border: '1px solid #eee', 100 | boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)" 101 | }); 102 | this.$message.css({ 103 | fontSize: '13px', 104 | textAlign: 'left' 105 | }); 106 | break; 107 | case 'bottom': 108 | this.$bar.css({ 109 | borderTop: '2px solid #eee', 110 | borderLeft: '2px solid #eee', 111 | borderRight: '2px solid #eee', 112 | borderBottom: '2px solid #eee', 113 | boxShadow: "0 -2px 4px rgba(0, 0, 0, 0.1)" 114 | }); 115 | break; 116 | default: 117 | this.$bar.css({ 118 | border: '2px solid #eee', 119 | boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)" 120 | }); 121 | break; 122 | } 123 | 124 | switch (this.options.type) { 125 | case 'alert': 126 | case 'notification': 127 | case 'warning': 128 | case 'error': 129 | case 'information': 130 | case 'success': 131 | default: 132 | this.$bar.css({ 133 | backgroundColor: '#DC569F', 134 | borderColor: '#DC569F', 135 | color: '#F5F5F5' 136 | }); 137 | this.$message.css({ 138 | fontWeight: 'bold' 139 | }); 140 | break; 141 | } 142 | }, 143 | callback: { 144 | onShow: function () { 145 | 146 | }, 147 | onClose: function () { 148 | 149 | } 150 | } 151 | }; -------------------------------------------------------------------------------- /controllers/referral_code_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gorilla/mux" 6 | "github.com/larryprice/refermadness/models" 7 | "github.com/larryprice/refermadness/utils" 8 | "gopkg.in/mgo.v2" 9 | "gopkg.in/mgo.v2/bson" 10 | "gopkg.in/unrolled/render.v1" 11 | "io/ioutil" 12 | "net/http" 13 | ) 14 | 15 | type ReferralCodeControllerImpl struct { 16 | currentUser utils.CurrentUserAccessor 17 | renderer *render.Render 18 | database utils.DatabaseAccessor 19 | } 20 | 21 | func NewReferralCodeController(currentUser utils.CurrentUserAccessor, renderer *render.Render, 22 | database utils.DatabaseAccessor) *ReferralCodeControllerImpl { 23 | return &ReferralCodeControllerImpl{ 24 | currentUser: currentUser, 25 | renderer: renderer, 26 | database: database, 27 | } 28 | } 29 | 30 | func (rc *ReferralCodeControllerImpl) Register(router *mux.Router) { 31 | router.HandleFunc("/codes", rc.create).Methods("POST") 32 | router.HandleFunc("/codes/random", rc.random).Methods("GET") 33 | router.HandleFunc("/codes/{id}/report", rc.report).Methods("GET") 34 | } 35 | 36 | func (rc *ReferralCodeControllerImpl) create(w http.ResponseWriter, r *http.Request) { 37 | body, _ := ioutil.ReadAll(r.Body) 38 | var values map[string]string 39 | json.Unmarshal(body, &values) 40 | 41 | // verify serviceID 42 | rawServiceID := values["serviceId"] 43 | if !bson.IsObjectIdHex(rawServiceID) { 44 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 45 | "error": "Bad service ID", 46 | }) 47 | return 48 | } 49 | 50 | db := rc.database.Get(r) 51 | service := new(models.Service) 52 | serviceID := bson.ObjectIdHex(rawServiceID) 53 | if err := service.FindByID(serviceID, db); err != nil || !service.ID.Valid() { 54 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 55 | "error": "Bad service request", 56 | }) 57 | return 58 | } 59 | 60 | userID := rc.currentUser.Get(r).ID 61 | code := values["code"] 62 | refCode := new(models.ReferralCode) 63 | if refCode.FindByUserAndService(userID, serviceID, db); refCode.ID.Valid() { 64 | if code == "" { 65 | analytics := new(models.Analytics) 66 | defer analytics.AddDeletedReferralCode(refCode, db) 67 | defer refCode.Delete(db) 68 | 69 | rc.renderer.JSON(w, http.StatusOK, nil) 70 | return 71 | } 72 | 73 | if err := refCode.Edit(code, db); err != nil { 74 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 75 | "error": err.Error(), 76 | }) 77 | return 78 | } 79 | } else { 80 | if code == "" { 81 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 82 | "error": "Empty referral code not allowed", 83 | }) 84 | return 85 | } 86 | refCode = models.NewReferralCode(code, rc.currentUser.Get(r).ID, service.ID) 87 | if err := refCode.Save(db); err != nil { 88 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{ 89 | "error": err.Error(), 90 | }) 91 | return 92 | } 93 | } 94 | 95 | rc.renderer.JSON(w, http.StatusCreated, refCode) 96 | } 97 | 98 | func (rc *ReferralCodeControllerImpl) random(w http.ResponseWriter, r *http.Request) { 99 | serviceID := r.FormValue("sid") 100 | if !bson.IsObjectIdHex(serviceID) { 101 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid service ID."}) 102 | return 103 | } 104 | 105 | if refCode := rc.randomCode(bson.ObjectIdHex(serviceID), rc.database.Get(r), w); refCode.ID.Valid() { 106 | rc.renderer.JSON(w, http.StatusOK, refCode) 107 | } 108 | } 109 | 110 | func (rc *ReferralCodeControllerImpl) randomCode(serviceID bson.ObjectId, db *mgo.Database, w http.ResponseWriter) *models.ReferralCode { 111 | refCode := new(models.ReferralCode) 112 | 113 | service := new(models.Service) 114 | if err := service.FindByID(serviceID, db); err != nil { 115 | rc.renderer.JSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) 116 | return refCode 117 | } 118 | if !service.ID.Valid() { 119 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{"error": "No such service."}) 120 | return refCode 121 | } 122 | 123 | if err := refCode.FindRandom(service.ID, db); err != nil { 124 | rc.renderer.JSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) 125 | return refCode 126 | } 127 | defer refCode.WasViewed(db) 128 | 129 | return refCode 130 | } 131 | 132 | func (rc *ReferralCodeControllerImpl) report(w http.ResponseWriter, r *http.Request) { 133 | u := rc.currentUser.Get(r) 134 | if u == nil { 135 | rc.renderer.JSON(w, http.StatusUnauthorized, map[string]string{"error": "Must be logged in to report invalid codes"}) 136 | return 137 | } 138 | 139 | if !bson.IsObjectIdHex(mux.Vars(r)["id"]) { 140 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid BSON ID"}) 141 | return 142 | } 143 | code := new(models.ReferralCode) 144 | db := rc.database.Get(r) 145 | if err := code.FindByID(bson.ObjectIdHex(mux.Vars(r)["id"]), db); err != nil { 146 | rc.renderer.JSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) 147 | return 148 | } 149 | 150 | if !code.ID.Valid() { 151 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{"error": "No such referral code"}) 152 | return 153 | } 154 | 155 | if u.ID == code.UserID { 156 | rc.renderer.JSON(w, http.StatusBadRequest, map[string]string{"error": "Don't report your own code, silly"}) 157 | return 158 | } 159 | 160 | defer code.WasReported(u.ID, db) 161 | 162 | flag := models.NewReferralCodeFlag(code.ID, u.ID) 163 | defer flag.Save(db) 164 | 165 | if newCode := rc.randomCode(code.ServiceID, db, w); newCode.ID.Valid() { 166 | rc.renderer.JSON(w, http.StatusOK, newCode) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /public/scripts/create-service.jsx: -------------------------------------------------------------------------------- 1 | var FormError = React.createClass({ 2 | render: function() { 3 | return ( 4 |
5 | 6 | (error) 7 |
8 | ); 9 | } 10 | }); 11 | 12 | var CreateServiceName = React.createClass({ 13 | render: function() { 14 | return ( 15 |
16 | 17 |
18 | 19 | 20 |
21 |
22 | ); 23 | } 24 | }); 25 | 26 | var CreateServiceURL = React.createClass({ 27 | render: function() { 28 | return ( 29 |
30 | 31 |
32 |
33 |
https://
34 | 35 |
36 | 37 |
38 |
39 | ); 40 | } 41 | }); 42 | 43 | var CreateServiceDescription = React.createClass({ 44 | render: function() { 45 | return ( 46 |
47 | 48 |
49 | 50 | 51 |
52 |
53 | ); 54 | } 55 | }); 56 | 57 | var CreateServiceButton = React.createClass({ 58 | addService: function(e) { 59 | e.preventDefault(); 60 | 61 | if ($("body").attr("data-logged-in") !== "true") { 62 | $("#authenticate-panel").collapse("show"); 63 | $("#authenticate-panel")[0].scrollIntoView(); 64 | return; 65 | } 66 | 67 | $(".create-service-name, .create-service-description, .create-service-url").removeClass("has-error"); 68 | var validationError = false; 69 | if ($("#create-service-name").val() === "") { 70 | $(".create-service-name").addClass("has-error"); 71 | validationError = true; 72 | } 73 | if ($("#create-service-url").val() === "") { 74 | $(".create-service-url").addClass("has-error"); 75 | validationError = true; 76 | } 77 | if ($("#create-service-description").val() === "") { 78 | $(".create-service-description").addClass("has-error"); 79 | validationError = true; 80 | } 81 | 82 | $("#create-service-name, #create-service-description, #create-service-url").one("keyup", function() { 83 | $(this).closest(".form-group").removeClass("has-error"); 84 | }); 85 | 86 | if (validationError) { 87 | return; 88 | } 89 | 90 | $(".form-group .glyphicon").addClass("spin infinite"); 91 | var that = this; 92 | $.ajax({ 93 | url: "/service/create", 94 | method: "POST", 95 | contentType: "application/json", 96 | data: JSON.stringify({ 97 | name: $("#create-service-name").val(), 98 | description: $("#create-service-description").val(), 99 | url: $("#create-service-url").val() 100 | }), 101 | dataType: "json", 102 | success: function(service) { 103 | that.props.onServiceCreated(service); 104 | }, 105 | error: function(xhr) { 106 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 107 | }, 108 | complete: function() { 109 | $(".form-group .glyphicon").removeClass("spin infinite"); 110 | } 111 | }); 112 | }, 113 | render: function() { 114 | return ( 115 |
116 |
117 | 121 |
122 |
123 | ); 124 | } 125 | }); 126 | 127 | var CreateService = React.createClass({ 128 | componentDidMount: function() { 129 | if (this.props.fadeIn) { 130 | setTimeout(function() { 131 | $(".create-service").addClass("fade-in"); 132 | }); 133 | } else { 134 | $(".create-service").addClass("fade-in"); 135 | } 136 | }, 137 | handleCreation: function(data) { 138 | $(".create-service").removeClass("fade-in"); 139 | this.props.onCreated(data); 140 | }, 141 | render: function() { 142 | return ( 143 |
144 |
145 |
146 | Know a service we don't? 147 |
148 |
149 |
150 |
151 | Add it! 152 |
153 |
154 |
155 |
156 |

157 | This is for informational purposes. 158 |

159 |

160 | You'll have the chance to add your referral code after you define the service. 161 |

162 |
163 |
164 |
165 | 166 | 167 | 168 | 169 | 170 |
171 | ); 172 | } 173 | }); 174 | -------------------------------------------------------------------------------- /public/scripts/common.jsx: -------------------------------------------------------------------------------- 1 | var Result = React.createClass({ 2 | getInitialState: function() { 3 | return { 4 | service: this.props.data, 5 | }; 6 | }, 7 | viewFull: function() { 8 | this.props.onSelected(this.state.service); 9 | }, 10 | render: function() { 11 | return ( 12 |
13 |

14 | {this.state.service.Name} 15 |

16 |
17 | {this.state.service.Description} 18 |
19 |

20 | {this.state.service.URL} 21 |

22 |
23 | ); 24 | } 25 | }); 26 | 27 | var MoreResults = React.createClass({ 28 | render: function() { 29 | if (!this.props.isVisible) { 30 | return null; 31 | } 32 | 33 | return ( 34 |
35 |
36 | 40 |
41 |
42 | ); 43 | } 44 | }); 45 | 46 | var Title = React.createClass({ 47 | render: function() { 48 | return ( 49 |
50 | 51 | Refer Madness 52 | 53 |
54 | ) 55 | } 56 | }); 57 | 58 | var SmallTitle = React.createClass({ 59 | render: function() { 60 | return ( 61 |
62 | 63 | Refer Madness 64 | 65 |
66 | ) 67 | } 68 | }); 69 | 70 | var TitleArea = React.createClass({ 71 | render: function() { 72 | if (this.props.smallTitle) { 73 | return ( 74 |
75 | 76 |
77 | ) 78 | } else { 79 | return ( 80 |
81 | 82 | </div> 83 | ) 84 | } 85 | } 86 | }); 87 | 88 | var LoginButton = React.createClass({ 89 | togglePanel: function() { 90 | if (window.location.pathname === "/") { 91 | $(".title").toggleClass("shrink fast"); 92 | } 93 | $("#authenticate-panel").collapse('toggle'); 94 | }, 95 | render: function() { 96 | return ( 97 | <div className="col-xs-12 col-sm-2 text-center"> 98 | <button className="login-btn btn btn-default" data-toggle="collapse" onClick={this.togglePanel} 99 | aria-expanded="false" aria-controls="authenticate-panel"> 100 | <span className="glyphicon glyphicon-lock"></span> 101 | Sign Up or Log In 102 | </button> 103 | </div> 104 | ) 105 | } 106 | }); 107 | 108 | var AccountButton = React.createClass({ 109 | render: function() { 110 | return ( 111 | <a className="btn btn-default account-btn" href="/account"> 112 | <span className="glyphicon glyphicon-user"></span> 113 | Account 114 | </a> 115 | ); 116 | } 117 | }); 118 | 119 | var LogoutButton = React.createClass({ 120 | render: function() { 121 | return ( 122 | <a className="btn btn-default logout-btn" href="/logout"> 123 | <span className="glyphicon glyphicon-off"></span> 124 | Log out 125 | </a> 126 | ); 127 | } 128 | }); 129 | 130 | var AuthenticatePanel = React.createClass({ 131 | toggleFAQ: function() { 132 | $("#login-faq").collapse("toggle"); 133 | }, 134 | authenticate: function() { 135 | window.location.href = "/login?returnURL=" + encodeURIComponent(window.location.pathname + window.location.search); 136 | }, 137 | render: function() { 138 | return ( 139 | <div className="row collapse" id="authenticate-panel"> 140 | <div className="col-xs-12 text-center"> 141 | <h2><strong>Let's get you authenticated.</strong><span className="glyphicon glyphicon-question-sign" onClick={this.toggleFAQ}></span></h2> 142 | <div id="login-faq" className="container collapse"> 143 | <div className="login-faq-question"><strong>Why should I?</strong></div> 144 | <div className="login-faq-answer">Authentication helps prevent malicious users from submitting bad or duplicate referral codes and prevents robots from taking over the site.</div> 145 | <div className="login-faq-question"><strong>Why Google?</strong></div> 146 | <div className="login-faq-answer">Google has a respectable history of protecting user passwords. Authenticating with Google means that Refer Madness will never see your password. It also means one less password for you to remember (and, eventually, forget).</div> 147 | <div className="login-faq-question"><strong>Where's the legal information?</strong></div> 148 | <div className="login-faq-answer">You can view the privacy policy and terms of service on <a href="/legal">the legal page</a>.</div> 149 | </div> 150 | <button className="btn btn-default btn-lg btn-google" onClick={this.authenticate}> 151 | <span className="glyphicon google-plus"></span> 152 | Sign in with Google 153 | </button> 154 | <h5>By signing in using the link above, you agree to the <a href="/legal">Terms and Conditions</a>.</h5> 155 | </div> 156 | </div> 157 | ); 158 | } 159 | }); 160 | 161 | var Header = React.createClass({ 162 | getInitialState: function() { 163 | return { 164 | loggedIn: $("body").attr("data-logged-in") === "true" 165 | } 166 | }, 167 | render: function() { 168 | if (this.state.loggedIn) { 169 | return ( 170 | <div className="header"> 171 | <div className="container-fluid"> 172 | <div className="row"> 173 | <TitleArea smallTitle={this.props.smallTitle} /> 174 | <div className="text-center"> 175 | <AccountButton /> 176 | <LogoutButton /> 177 | </div> 178 | </div> 179 | </div> 180 | </div> 181 | ) 182 | } else { 183 | return ( 184 | <div className="header"> 185 | <div className="container-fluid"> 186 | <div className="row"> 187 | <TitleArea smallTitle={this.props.smallTitle} /> 188 | <LoginButton /> 189 | </div> 190 | <AuthenticatePanel /> 191 | </div> 192 | </div> 193 | ) 194 | } 195 | } 196 | }); -------------------------------------------------------------------------------- /public/scripts/create-service.js: -------------------------------------------------------------------------------- 1 | var FormError = React.createClass({displayName: "FormError", 2 | render: function() { 3 | return ( 4 | React.createElement("div", {className: "form-error"}, 5 | React.createElement("span", {className: "glyphicon glyphicon-remove form-control-feedback", "aria-hidden": "true"}), 6 | React.createElement("span", {className: "sr-only"}, "(error)") 7 | ) 8 | ); 9 | } 10 | }); 11 | 12 | var CreateServiceName = React.createClass({displayName: "CreateServiceName", 13 | render: function() { 14 | return ( 15 | React.createElement("div", {className: "form-group create-service-name"}, 16 | React.createElement("label", {className: "col-sm-3 col-xs-12 control-label", for: "create-service-name"}, "Name"), 17 | React.createElement("div", {className: "col-sm-9 col-xs-12"}, 18 | React.createElement("input", {type: "text", className: "form-control input-lg", id: "create-service-name", placeholder: "Name of the service..."}), 19 | React.createElement(FormError, null) 20 | ) 21 | ) 22 | ); 23 | } 24 | }); 25 | 26 | var CreateServiceURL = React.createClass({displayName: "CreateServiceURL", 27 | render: function() { 28 | return ( 29 | React.createElement("div", {className: "form-group create-service-url"}, 30 | React.createElement("label", {className: "col-sm-3 col-xs-12 control-label", for: "create-service-url"}, "Home Page"), 31 | React.createElement("div", {className: "col-sm-9 col-xs-12"}, 32 | React.createElement("div", {className: "input-group"}, 33 | React.createElement("div", {className: "input-group-addon"}, "https://"), 34 | React.createElement("input", {type: "text", className: "form-control input-lg", id: "create-service-url", placeholder: "yourservice.com"}) 35 | ), 36 | React.createElement(FormError, null) 37 | ) 38 | ) 39 | ); 40 | } 41 | }); 42 | 43 | var CreateServiceDescription = React.createClass({displayName: "CreateServiceDescription", 44 | render: function() { 45 | return ( 46 | React.createElement("div", {className: "form-group create-service-description"}, 47 | React.createElement("label", {className: "col-sm-3 col-xs-12 control-label", for: "create-service-description"}, "Description"), 48 | React.createElement("div", {className: "col-sm-9 col-xs-12"}, 49 | React.createElement("input", {type: "text", className: "form-control input-lg", id: "create-service-description", placeholder: "Describe the service in a few words..."}), 50 | React.createElement(FormError, null) 51 | ) 52 | ) 53 | ); 54 | } 55 | }); 56 | 57 | var CreateServiceButton = React.createClass({displayName: "CreateServiceButton", 58 | addService: function(e) { 59 | e.preventDefault(); 60 | 61 | if ($("body").attr("data-logged-in") !== "true") { 62 | $("#authenticate-panel").collapse("show"); 63 | $("#authenticate-panel")[0].scrollIntoView(); 64 | return; 65 | } 66 | 67 | $(".create-service-name, .create-service-description, .create-service-url").removeClass("has-error"); 68 | var validationError = false; 69 | if ($("#create-service-name").val() === "") { 70 | $(".create-service-name").addClass("has-error"); 71 | validationError = true; 72 | } 73 | if ($("#create-service-url").val() === "") { 74 | $(".create-service-url").addClass("has-error"); 75 | validationError = true; 76 | } 77 | if ($("#create-service-description").val() === "") { 78 | $(".create-service-description").addClass("has-error"); 79 | validationError = true; 80 | } 81 | 82 | $("#create-service-name, #create-service-description, #create-service-url").one("keyup", function() { 83 | $(this).closest(".form-group").removeClass("has-error"); 84 | }); 85 | 86 | if (validationError) { 87 | return; 88 | } 89 | 90 | $(".form-group .glyphicon").addClass("spin infinite"); 91 | var that = this; 92 | $.ajax({ 93 | url: "/service/create", 94 | method: "POST", 95 | contentType: "application/json", 96 | data: JSON.stringify({ 97 | name: $("#create-service-name").val(), 98 | description: $("#create-service-description").val(), 99 | url: $("#create-service-url").val() 100 | }), 101 | dataType: "json", 102 | success: function(service) { 103 | that.props.onServiceCreated(service); 104 | }, 105 | error: function(xhr) { 106 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 107 | }, 108 | complete: function() { 109 | $(".form-group .glyphicon").removeClass("spin infinite"); 110 | } 111 | }); 112 | }, 113 | render: function() { 114 | return ( 115 | React.createElement("div", {className: "form-group"}, 116 | React.createElement("div", {className: "col-sm-offset-3 col-sm-9 col-xs-12"}, 117 | React.createElement("button", {type: "submit", className: "btn btn-default btn-lg", onClick: this.addService}, 118 | React.createElement("span", {className: "glyphicon glyphicon-plus"}), 119 | "Create Service" 120 | ) 121 | ) 122 | ) 123 | ); 124 | } 125 | }); 126 | 127 | var CreateService = React.createClass({displayName: "CreateService", 128 | componentDidMount: function() { 129 | if (this.props.fadeIn) { 130 | setTimeout(function() { 131 | $(".create-service").addClass("fade-in"); 132 | }); 133 | } else { 134 | $(".create-service").addClass("fade-in"); 135 | } 136 | }, 137 | handleCreation: function(data) { 138 | $(".create-service").removeClass("fade-in"); 139 | this.props.onCreated(data); 140 | }, 141 | render: function() { 142 | return ( 143 | React.createElement("div", {className: "create-service"}, 144 | React.createElement("div", {className: "row"}, 145 | React.createElement("div", {className: "create-service-title col-xs-12"}, 146 | "Know a service we don't?" 147 | ) 148 | ), 149 | React.createElement("div", {className: "row"}, 150 | React.createElement("div", {className: "create-service-subtitle col-xs-12"}, 151 | "Add it!" 152 | ) 153 | ), 154 | React.createElement("div", {className: "row"}, 155 | React.createElement("div", {className: "col-xs-12 create-service-information"}, 156 | React.createElement("h4", null, 157 | "This is for informational purposes." 158 | ), 159 | React.createElement("h4", null, 160 | "You'll have the chance to add your referral code after you define the service." 161 | ) 162 | ) 163 | ), 164 | React.createElement("form", {className: "form-horizontal"}, 165 | React.createElement(CreateServiceName, null), 166 | React.createElement(CreateServiceURL, null), 167 | React.createElement(CreateServiceDescription, null), 168 | React.createElement(CreateServiceButton, {onServiceCreated: this.handleCreation}) 169 | ) 170 | ) 171 | ); 172 | } 173 | }); 174 | -------------------------------------------------------------------------------- /public/css/style.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAMA,UAKC;EAJC,WAAW,EAAE,MAAM;EACnB,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,GAAG;EAChB,GAAG,EAAE,yFAAa;AAGpB,UAKC;EAJC,WAAW,EAAE,SAAS;EACtB,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,GAAG;EAChB,GAAG,EAAE,4HAAgB;AAGvB;;kBAEmB;EACjB,gBAAgB,EAAE,WAAW;EAC7B,KAAK,EAAE,OAAO;EACd,YAAY,EAAE,UAAU;EACxB,WAAW,EAAE,MAAM;;AAGrB;;iBAEkB;EAChB,gBAAgB,EAAE,OAAO;;AAG3B,kBAAmB;EACjB,OAAO,EAAE,IAAI;;AAGf,YAAa;EACX,KAAK,EAAE,QAAQ;EAEf,wBAAQ;IACN,KAAK,EAAE,QAAQ;;AAInB,yBAA0B;EACxB,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,MAAM;EAClB,MAAM,EAAE,OAAO;;AAGjB,yBAA0B;EACxB,iBAAkB;IAChB,KAAK,EAAE,GAAG;AAId,yBAA0B;EACxB,iBAAkB;IAChB,KAAK,EAAE,GAAG;AAKZ,eAAO;EACL,cAAc,EAAE,YAAY;EAC5B,kBAAkB,EAAE,EAAE;EACtB,yBAAyB,EAAE,MAAM;EACjC,sBAAsB,EAAE,YAAY;EACpC,0BAA0B,EAAE,EAAE;EAC9B,iCAAiC,EAAE,MAAM;EAEzC,wBAAW;IACT,yBAAyB,EAAE,QAAQ;IACnC,iCAAiC,EAAE,QAAQ;EAG7C,oBAAO;IACL,kBAAkB,EAAE,KAAK;IACzB,0BAA0B,EAAE,KAAK;AAIrC,gBAAQ;EACN,cAAc,EAAE,KAAK;EACrB,kBAAkB,EAAE,KAAK;EACzB,yBAAyB,EAAE,CAAC;EAC5B,yBAAyB,EAAE,MAAM;EACjC,gBAAgB,EAAE,OAAO;EAEzB,sBAAsB,EAAE,KAAK;EAC7B,0BAA0B,EAAE,KAAK;EACjC,iCAAiC,EAAE,CAAC;EACpC,iCAAiC,EAAE,MAAM;;AAI7C,uBAOC;EANC,IAAK;IACH,SAAS,EAAE,YAAY;EAEzB,EAAG;IACD,SAAS,EAAE,cAAc;AAK7B,+BAOC;EANC,IAAK;IACH,iBAAiB,EAAE,YAAY;EAEjC,EAAG;IACD,iBAAiB,EAAE,cAAc;AAKnC,QAAE;EACA,SAAS,EAAE,IAAI;;ACxHnB,QAAS;EACP,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,kBAAkB;EAC/B,gBAAgB,EDHV,OAAO;ECIb,KAAK,EAAE,UAAU;;AAGnB,MAAO;EACL,UAAU,EAAE,CAAC;EACb,WAAW,EAAE,KAAK;EAClB,WAAW,EAAE,kBAAkB;EAC/B,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,YAAY;EAExB,WAAO;IACL,UAAU,EAAE,YAAY;EAG1B,QAAE;IACA,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,IAAI;EAGvB,aAAS;IACP,SAAS,EAAE,IAAI;;AAInB,qCAAsC;EACpC,UAAU,EAAE,GAAG;EAEf,sEAAW;IACT,SAAS,EAAE,IAAI;IACf,YAAY,EAAE,IAAI;;AAItB,YAAa;EACX,YAAY,EAAE,IAAI;;AAGpB,mBAAoB;EAClB,gBAAgB,EDxCD,OAAO;EC0CtB,4CAAyB;IACvB,SAAS,EAAE,IAAI;IACf,cAAc,EAAE,GAAG;IACnB,MAAM,EAAE,OAAO;EAGjB,0BAAO;IACL,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,KAAK;IACjB,aAAa,EAAE,IAAI;IAEnB,uCAAa;MACX,gBAAgB,EAAE,2BAA2B;MAC7C,iBAAiB,EAAE,SAAS;MAE5B,eAAe,EAAE,SAAS;MAE1B,KAAK,EAAE,IAAI;MACX,MAAM,EAAE,IAAI;MACZ,GAAG,EAAE,IAAI;MACT,YAAY,EAAE,IAAI;EAKpB,oGAAuC;IACrC,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,IAAI;EAGlB,gDAAkB;IAChB,WAAW,EAAE,GAAG;;AAKtB,yBAA0B;EACxB,MAAO;IACL,SAAS,EAAE,IAAI;AAInB,yBAA0B;EACxB,MAAO;IACL,SAAS,EAAE,KAAK;AAIpB,UAAW;EACT,WAAW,EAAE,kBAAkB;EAC/B,gBAAgB,ED9FV,OAAO;EC+Fb,KAAK,EAAE,UAAU;;AAGnB,IAAK;EACH,QAAQ,EAAE,QAAQ;EAClB,UAAU,EAAE,IAAI;;AAGlB,IAAK;EACH,aAAa,EAAE,IAAI;;AAGrB,OAAQ;EACN,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,CAAC;EACP,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,UAAU;EACjB,gBAAgB,EDlHV,OAAO;ECoHb,aAAM;IACJ,SAAS,EAAG,IAAI;EAGlB,kBAAW;IACT,UAAU,EAAE,qBAAqB;IACjC,OAAO,EAAE,IAAI;IACb,cAAc,EAAE,CAAC;EAGnB,SAAE;IACA,KAAK,EAAE,UAAU;;AC/HrB,aAAc;EACZ,gBAAgB,EFDV,OAAO;EEGb,eAAE;IACA,WAAW,EAAE,IAAI;EAGnB,iBAAI;IACF,WAAW,EAAE,IAAI;;AAIrB,yBAA0B;EACxB,UAAU,EAAE,CAAC;;AAGf,eAAgB;EACd,UAAU,EAAE,GAAG;;AAGjB,cAAe;EACb,MAAM,EAAE,SAAS;EACjB,YAAY,EAAE,IAAI;EAClB,WAAW,EAAE,IAAI;EACjB,aAAa,EAAE,GAAG;EAClB,SAAS,EAAE,UAAU;EACrB,UAAU,EAAE,KAAK;EAEjB,UAAU,EAAE,WAAW;EAEvB,uBAAW;IACT,OAAO,EAAE,CAAC;EAGZ,oBAAQ;IACN,MAAM,EAAE,OAAO;;AAInB,WAAY;EACV,UAAU,EAAE,UAAU;EAEtB,oBAAW;IACT,OAAO,EAAE,CAAC;;AAKZ,wBAAW;EACT,YAAY,EAAE,IAAI;EAClB,GAAG,EAAE,GAAG;;AAIZ,qBAAsB;EACpB,UAAU,EAAE,MAAM;EAElB,6BAAU;IACR,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,CAAC;IACZ,OAAO,EAAE,CAAC;;AAKZ,wBAAG;EACD,WAAW,EAAE,IAAI;AAGnB,gCAAW;EACT,UAAU,EAAE,IAAI;EAChB,SAAS,EAAE,IAAI;;ACvEnB,YAAa;EACX,UAAU,EAAE,CAAC;EAEb,0BAAc;IACZ,SAAS,EAAE,IAAI;IACf,WAAW,EAAE,IAAI;EAGnB,kCAAsB;IACpB,UAAU,EAAE,GAAG;IAEf,yDAAuB;MACrB,UAAU,EAAE,KAAK;MACjB,UAAU,EAAE,WAAW;MAEvB,gEAAO;QACL,YAAY,EAAE,GAAG;QACjB,WAAW,EAAE,GAAG;MAGlB,iEAAU;QACR,OAAO,EAAE,CAAC;MAGZ,kEAAW;QACT,OAAO,EAAE,CAAC;MAGZ,oEAAW;QACT,YAAY,EAAE,IAAI;MAGpB,0EAAiB;QACf,OAAO,EAAE,MAAM;QAEf,iGAAuB;UACrB,SAAS,EAAE,IAAI;UACf,cAAc,EAAE,MAAM;QAGxB,8FAAoB;UAClB,WAAW,EAAE,CAAC;EAMtB,2BAAe;IACb,UAAU,EAAE,WAAW;IAEvB,mCAAU;MACR,OAAO,EAAE,CAAC;IAGZ,oCAAW;MACT,OAAO,EAAE,CAAC;EAOd,2BAAe;IACb,SAAS,EAAE,UAAU;EAIrB,iDAAkB;IAChB,YAAY,EAAE,IAAI;EAIlB,wDAAW;IACT,GAAG,EAAE,IAAI;EAIb,6CAAc;IACZ,OAAO,EAAE,YAAY;IACrB,KAAK,EAAE,IAAI;IACX,QAAQ,EAAE,MAAM;IAChB,WAAW,EAAE,MAAM;IACnB,UAAU,EAAE,QAAQ;IAEpB,qDAAU;MACR,KAAK,EAAE,CAAC;MACR,QAAQ,EAAE,MAAM;MAChB,YAAY,EAAE,eAAe;MAC7B,WAAW,EAAE,MAAM;EAKzB,qDAAyC;IACvC,UAAU,EAAE,IAAI;IAChB,SAAS,EAAE,GAAG;EAGhB,iEAAwC;IACtC,UAAU,EAAE,GAAG;IAEf,6FAAc;MACZ,UAAU,EAAE,GAAG;MACf,MAAM,EAAE,IAAI;MAEZ,6GAAU;QACR,sBAAsB,EAAE,CAAC;QACzB,yBAAyB,EAAE,CAAC;QAC5B,iBAAiB,EAAE,SAAS;MAG9B,mHAAW;QACT,UAAU,EAAE,YAAY;QAExB,mIAAU;UACR,OAAO,EAAE,CAAC;QAGZ,qIAAW;UACT,OAAO,EAAE,CAAC;IAKhB,iGAAgB;MACd,OAAO,EAAE,YAAY;MACrB,KAAK,EAAE,IAAI;MACX,UAAU,EAAE,gCAAgC;MAC5C,uBAAuB,EAAE,CAAC;MAC1B,0BAA0B,EAAE,CAAC;MAE7B,iHAAU;QACR,OAAO,EAAE,CAAC;QACV,KAAK,EAAE,CAAC;QACR,OAAO,EAAE,CAAC;IAId,yBAA0B;MACxB,6HAA8B;QAC5B,KAAK,EAAE,IAAI;MAGb,yHAA4B;QAC1B,SAAS,EAAE,IAAI;;AAMvB,gBAIC;EAHC,EAAG;IAAE,SAAS,EAAE,QAAQ;EACxB,GAAI;IAAC,SAAS,EAAE,UAAU;EAC1B,IAAK;IAAE,SAAS,EAAE,QAAQ;AAG5B,wBAIC;EAHC,EAAG;IAAE,SAAS,EAAE,QAAQ;EACxB,GAAI;IAAC,SAAS,EAAE,UAAU;EAC1B,IAAK;IAAE,SAAS,EAAE,QAAQ;AC/J5B,sCAAuC;EACrC,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,KAAK;EACb,eAAe,EAAE,KAAK;EACtB,OAAO,EAAE,GAAG;EAEZ,wDAAQ;IACN,UAAU,EACP,yQAKuB;IAO1B,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,QAAQ,EAAE,QAAQ;IAClB,MAAM,EAAE,CAAC;IACT,IAAI,EAAE,CAAC;IACP,OAAO,EAAE,EAAE;;AAIf,WAAY;EACV,UAAU,EAAE,mDAAmD;;AAGjE,UAAW;EACT,UAAU,EAAE,kDAAkD;;AAGhE,aAAc;EACZ,UAAU,EAAE,qDAAqD;;AAGnE,WAAY;EACV,UAAU,EAAE,GAAG;EACf,aAAa,EAAE,GAAG;EAElB,cAAG;IACD,SAAS,EAAE,IAAI;IACf,WAAW,EAAE,IAAI;IACjB,UAAU,EAAE,GAAG;;AAKjB,+BAAa;EACX,SAAS,EAAE,IAAI;EACf,gBAAgB,EAAE,WAAW;EAC7B,KAAK,EAAE,OAAO;EACd,YAAY,EAAE,UAAU;EACxB,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,GAAG;EAClB,WAAW,EAAE,MAAM;AAGrB,oCAAkB;EAChB,GAAG,EAAE,GAAG;EACR,YAAY,EAAE,IAAI;;AAItB,wBAAyB;EACvB,UAAU,EAAE,gBAAgB;;AAG9B;;cAEe;EACb,gBAAgB,EJ3EJ,OAAO;;AI8ErB;kBACmB;EACjB,gBAAgB,EJ/ED,OAAO;;AImFtB,uCAAK;EACH,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,IAAI;AAGtB,2DAAe;EACb,UAAU,EAAE,IAAI;;AAIpB,aAAc;EACZ,UAAU,EAAE,KAAK;;AAGnB,YAAa;EACX,OAAO,EAAE,GAAG;EACZ,UAAU,EAAE,IAAI;;ACrGlB,eAAgB;EAyCd,UAAU,EAAE,UAAU;EACtB,OAAO,EAAE,CAAC;EAzCV,8BAAe;IACb,SAAS,EAAE,IAAI;EAIf,yCAAe;IACb,KAAK,EAAE,UAAU;EAGnB,wCAAc;IACZ,UAAU,EAAE,OAAO;EAIvB,sCAAuB;IACrB,KAAK,EAAE,KAAK;IAEZ,YAAY,EAAE,IAAI;IAClB,GAAG,EAAE,GAAG;IACR,SAAS,EAAE,IAAI;EAGjB,iDAAkC;IAChC,KAAK,EAAE,OAAO;EAGhB,wCAAyB;IACvB,SAAS,EAAE,IAAI;IACf,WAAW,EAAE,IAAI;IACjB,UAAU,EAAE,KAAK;EAGnB,qCAAsB;IACpB,SAAS,EAAE,IAAI;EAGjB,2CAA4B;IAC1B,aAAa,EAAE,KAAK;EAMtB,uBAAU;IACR,OAAO,EAAE,CAAC;EAGZ,sBAAO;IACL,KAAK,EAAE,IAAI;IACX,UAAU,EAAE,IAAI;IAEhB,iCAAW;MACT,YAAY,EAAE,IAAI;MAClB,GAAG,EAAE,GAAG;;AAKd,iCAAkC;EAChC,UAAU,EAAE,CAAC;;AC1DX,gCAAG;EACD,UAAU,EAAE,GAAG;AAGjB,oCAAO;EACL,WAAW,EAAE,MAAM;EAEnB,+CAAW;IACT,YAAY,EAAE,IAAI;IAClB,GAAG,EAAE,GAAG;EAGV,mDAAiB;IACf,UAAU,EAAE,GAAG;IACf,aAAa,EAAE,GAAG;IAElB,UAAU,EAAE,+CAA+C;IAC3D,OAAO,EAAE,CAAC;IAEV,4DAAW;MACT,OAAO,EAAE,CAAC;MACV,MAAM,EAAE,CAAC;MACT,UAAU,EAAE,KAAK;AAKvB,qDAAwB;EACtB,WAAW,EAAE,IAAI;AAGnB,yDAA4B;EAC1B,UAAU,EAAE,GAAG;EAEf,UAAU,EAAE,WAAW;EACvB,OAAO,EAAE,CAAC;EAEV,kEAAW;IACT,OAAO,EAAE,CAAC;EAGZ,sFAA6B;IAC3B,SAAS,EAAE,IAAI;IACf,YAAY,EAAE,GAAG;EAGnB,sEAAa;IACX,gBAAgB,EAAE,2BAA2B;IAC7C,iBAAiB,EAAE,SAAS;IAE5B,eAAe,EAAE,SAAS;IAE1B,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,GAAG,EAAE,IAAI;IACT,YAAY,EAAE,IAAI;AAItB,wKAA+E;EAC7E,aAAa,EAAE,GAAG;EAElB,6LAAO;IACL,UAAU,EAAE,GAAG;IAEf,wNAAW;MACT,OAAO,EAAE,EAAE;MACX,cAAc,EAAE,IAAI;EAIxB,yMAAa;IACX,UAAU,EAAE,KAAK;EAGnB,mPAAyB;IACvB,WAAW,EAAE,GAAG;AAKlB,0DAAK;EACH,aAAa,EAAE,CAAC;AAGlB,iEAAY;EACV,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,IAAI;AAKzB,kCAAqB;EACnB,UAAU,EAAE,sBAAsB;EAClC,WAAW,EAAE,KAAK;EAElB,uCAAK;IACH,aAAa,EAAE,KAAK", 4 | "sources": ["_common.scss","_layout.scss","_search.scss","_service.scss","_landing.scss","_create-service.scss","_account.scss"], 5 | "names": [], 6 | "file": "style.css" 7 | } -------------------------------------------------------------------------------- /controllers/account_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/dgrijalva/jwt-go" 7 | "github.com/gorilla/mux" 8 | "github.com/larryprice/refermadness/models" 9 | "github.com/larryprice/refermadness/utils" 10 | "gopkg.in/mgo.v2/bson" 11 | "gopkg.in/unrolled/render.v1" 12 | "html/template" 13 | "io/ioutil" 14 | "net/http" 15 | "net/url" 16 | "strconv" 17 | ) 18 | 19 | type AccountControllerImpl struct { 20 | clientID string 21 | clientSecret string 22 | scheme string 23 | 24 | session utils.SessionManager 25 | database utils.DatabaseAccessor 26 | currentUser utils.CurrentUserAccessor 27 | basePage utils.BasePageCreator 28 | renderer *render.Render 29 | } 30 | 31 | func NewAccountController(clientID, clientSecret string, isDevelopment bool, session utils.SessionManager, 32 | database utils.DatabaseAccessor, currentUser utils.CurrentUserAccessor, basePage utils.BasePageCreator, 33 | renderer *render.Render) *AccountControllerImpl { 34 | scheme := "http" 35 | if !isDevelopment { 36 | scheme += "s" 37 | } 38 | 39 | return &AccountControllerImpl{ 40 | clientID: clientID, 41 | clientSecret: clientSecret, 42 | scheme: scheme, 43 | session: session, 44 | database: database, 45 | currentUser: currentUser, 46 | basePage: basePage, 47 | renderer: renderer, 48 | } 49 | } 50 | 51 | func (ac *AccountControllerImpl) Register(router *mux.Router) { 52 | // auth 53 | router.HandleFunc("/login", ac.login) 54 | router.HandleFunc("/logout", ac.logout) 55 | router.HandleFunc("/oauth2callback", ac.oauth2) 56 | 57 | // account 58 | router.HandleFunc("/account", ac.account) 59 | router.HandleFunc("/account/switch", ac.switchAccounts) 60 | router.HandleFunc("/account/delete", ac.deleteAccount) 61 | router.HandleFunc("/account/services", ac.services) 62 | } 63 | 64 | func (ac *AccountControllerImpl) login(w http.ResponseWriter, r *http.Request) { 65 | if returnURL := r.FormValue("returnURL"); returnURL != "" { 66 | ac.session.Set(r, "RedirectAfterLogin", returnURL) 67 | } 68 | 69 | http.Redirect(w, r, "https://accounts.google.com/o/oauth2/auth?scope=email&redirect_uri="+ 70 | ac.scheme+"%3A%2F%2F"+r.Host+"%2foauth2callback"+"&response_type=code&client_id="+ac.clientID, 71 | http.StatusTemporaryRedirect) 72 | } 73 | 74 | func (ac *AccountControllerImpl) oauth2(w http.ResponseWriter, r *http.Request) { 75 | redirectTo := "/" 76 | if returnURL := ac.session.Get(r, "RedirectAfterLogin"); returnURL != "" { 77 | ac.session.Delete(r, "RedirectAfterLogin") 78 | redirectTo = returnURL 79 | } 80 | 81 | if r.FormValue("error") != "" { 82 | fmt.Println("Error in OAuth", r.FormValue("error")) 83 | http.Redirect(w, r, redirectTo, http.StatusFound) 84 | return 85 | } 86 | 87 | // send token request 88 | resp, err := http.PostForm("https://www.googleapis.com/oauth2/v3/token", 89 | url.Values{"code": {r.FormValue("code")}, "grant_type": {"authorization_code"}, "redirect_uri": {ac.scheme + "://" + r.Host + "/oauth2callback"}, 90 | "client_id": {ac.clientID}, "client_secret": {ac.clientSecret}}) 91 | 92 | if resp.StatusCode != http.StatusOK || err != nil { 93 | fmt.Println("Error in OAuth Access Token request", resp.StatusCode, err) 94 | http.Redirect(w, r, redirectTo, http.StatusFound) 95 | return 96 | } 97 | 98 | var result map[string]interface{} 99 | body, _ := ioutil.ReadAll(resp.Body) 100 | json.Unmarshal(body, &result) 101 | token, _ := jwt.Parse(result["id_token"].(string), func(token *jwt.Token) (interface{}, error) { 102 | return result["access_token"], nil 103 | }) 104 | 105 | if r.FormValue("state") != "updating" { 106 | ac.findOrCreateUser(token.Claims["email"].(string), result["access_token"].(string), r) 107 | } else { 108 | ac.switchUserAccount(token.Claims["email"].(string), result["access_token"].(string), r) 109 | } 110 | 111 | http.Redirect(w, r, redirectTo, http.StatusFound) 112 | } 113 | 114 | func (ac *AccountControllerImpl) findOrCreateUser(email, accessToken string, r *http.Request) { 115 | user := new(models.User) 116 | db := ac.database.Get(r) 117 | if user.FindByEmail(email, db); user.ID.Valid() { 118 | user.Update(email, accessToken, db) 119 | } else { 120 | user = models.NewUser(email, accessToken) 121 | user.Save(db) 122 | } 123 | ac.session.Set(r, "UserID", user.ID.Hex()) 124 | } 125 | 126 | func (ac *AccountControllerImpl) switchUserAccount(email, accessToken string, r *http.Request) { 127 | if user := ac.currentUser.Get(r); user != nil { 128 | user.Update(email, accessToken, ac.database.Get(r)) 129 | return 130 | } 131 | 132 | // error! 133 | // apparently we couldn't find the currently logged in user in the system 134 | ac.session.Delete(r, "UserID") 135 | } 136 | 137 | func (ac *AccountControllerImpl) logout(w http.ResponseWriter, r *http.Request) { 138 | ac.session.Delete(r, "UserID") 139 | http.Redirect(w, r, "/", http.StatusFound) 140 | } 141 | 142 | func (ac *AccountControllerImpl) switchAccounts(w http.ResponseWriter, r *http.Request) { 143 | ac.session.Set(r, "RedirectAfterLogin", "/account") 144 | 145 | http.Redirect(w, r, "https://accounts.google.com/o/oauth2/auth?scope=email&state=updating&redirect_uri="+ 146 | ac.scheme+"%3A%2F%2F"+r.Host+"%2foauth2callback"+"&response_type=code&client_id="+ac.clientID, 147 | http.StatusTemporaryRedirect) 148 | } 149 | 150 | func (ac *AccountControllerImpl) deleteAccount(w http.ResponseWriter, r *http.Request) { 151 | if user := ac.currentUser.Get(r); user != nil { 152 | db := ac.database.Get(r) 153 | 154 | analytics := new(models.Analytics) 155 | analytics.AddDeletedUser(user, db) 156 | 157 | user.Delete(db) 158 | } 159 | ac.session.Delete(r, "UserID") 160 | http.Redirect(w, r, "/", http.StatusFound) 161 | } 162 | 163 | func (ac *AccountControllerImpl) account(w http.ResponseWriter, r *http.Request) { 164 | if user := ac.currentUser.Get(r); user != nil { 165 | t, _ := template.ParseFiles("views/layout.html", "views/account.html") 166 | t.Execute(w, ac.basePage.Get(r)) 167 | } else { 168 | http.Error(w, "Users must be logged in to view the account page.", http.StatusUnauthorized) 169 | } 170 | } 171 | 172 | type serviceFetchResult struct { 173 | *models.Services 174 | Total int 175 | } 176 | 177 | func (ac *AccountControllerImpl) services(w http.ResponseWriter, r *http.Request) { 178 | user := ac.currentUser.Get(r) 179 | if user == nil { 180 | ac.renderer.JSON(w, http.StatusUnauthorized, map[string]string{ 181 | "error": "Must be logged in to view this page", 182 | }) 183 | } 184 | 185 | var limit int 186 | var err error 187 | 188 | if limit, err = strconv.Atoi(r.FormValue("limit")); err != nil { 189 | limit = 11 190 | } 191 | 192 | if limit > 50 { 193 | limit = 50 194 | } 195 | 196 | var skip int 197 | if skip, err = strconv.Atoi(r.FormValue("skip")); err != nil { 198 | skip = 0 199 | } 200 | 201 | var total int 202 | codes := new(models.ReferralCodes) 203 | if total, err = codes.FindByUserID(user.ID, limit, skip, ac.database.Get(r)); err != nil { 204 | ac.renderer.JSON(w, http.StatusInternalServerError, map[string]string{ 205 | "error": "There was an issue fetching codes from the database", 206 | }) 207 | } 208 | 209 | var serviceIDs []bson.ObjectId 210 | for _, code := range []models.ReferralCode(*codes) { 211 | serviceIDs = append(serviceIDs, code.ServiceID) 212 | } 213 | 214 | services := new(models.Services) 215 | if err = services.FindByIDs(serviceIDs, ac.database.Get(r)); err != nil { 216 | ac.renderer.JSON(w, http.StatusInternalServerError, map[string]string{ 217 | "error": "There was an issue fetching services from the database", 218 | }) 219 | } 220 | 221 | ac.renderer.JSON(w, http.StatusOK, serviceFetchResult{services, total}) 222 | } 223 | -------------------------------------------------------------------------------- /public/scripts/common.js: -------------------------------------------------------------------------------- 1 | var Result = React.createClass({displayName: "Result", 2 | getInitialState: function() { 3 | return { 4 | service: this.props.data, 5 | }; 6 | }, 7 | viewFull: function() { 8 | this.props.onSelected(this.state.service); 9 | }, 10 | render: function() { 11 | return ( 12 | React.createElement("div", {className: "search-result col-md-3-point-5 col-sm-6 col-xs-12", onClick: this.viewFull}, 13 | React.createElement("h2", null, 14 | this.state.service.Name 15 | ), 16 | React.createElement("h5", null, 17 | this.state.service.Description 18 | ), 19 | React.createElement("h4", null, 20 | this.state.service.URL 21 | ) 22 | ) 23 | ); 24 | } 25 | }); 26 | 27 | var MoreResults = React.createClass({displayName: "MoreResults", 28 | render: function() { 29 | if (!this.props.isVisible) { 30 | return null; 31 | } 32 | 33 | return ( 34 | React.createElement("div", {className: "more-results row"}, 35 | React.createElement("div", {className: "col-xs-12 text-center"}, 36 | React.createElement("button", {className: "btn btn-link btn-lg", onClick: this.props.onMore}, 37 | React.createElement("span", {className: "glyphicon glyphicon-chevron-down"}), 38 | "Load More" 39 | ) 40 | ) 41 | ) 42 | ); 43 | } 44 | }); 45 | 46 | var Title = React.createClass({displayName: "Title", 47 | render: function() { 48 | return ( 49 | React.createElement("div", {className: "title text-center"}, 50 | React.createElement("a", {href: "/", alt: "Return to home page."}, 51 | "Refer Madness" 52 | ) 53 | ) 54 | ) 55 | } 56 | }); 57 | 58 | var SmallTitle = React.createClass({displayName: "SmallTitle", 59 | render: function() { 60 | return ( 61 | React.createElement("div", {className: "shrink title text-center"}, 62 | React.createElement("a", {href: "/", alt: "Return to home page."}, 63 | "Refer Madness" 64 | ) 65 | ) 66 | ) 67 | } 68 | }); 69 | 70 | var TitleArea = React.createClass({displayName: "TitleArea", 71 | render: function() { 72 | if (this.props.smallTitle) { 73 | return ( 74 | React.createElement("div", {className: "col-sm-offset-2 col-sm-8 col-xs-12"}, 75 | React.createElement(SmallTitle, null) 76 | ) 77 | ) 78 | } else { 79 | return ( 80 | React.createElement("div", {className: "col-sm-offset-2 col-sm-8 col-xs-12"}, 81 | React.createElement(Title, null) 82 | ) 83 | ) 84 | } 85 | } 86 | }); 87 | 88 | var LoginButton = React.createClass({displayName: "LoginButton", 89 | togglePanel: function() { 90 | if (window.location.pathname === "/") { 91 | $(".title").toggleClass("shrink fast"); 92 | } 93 | $("#authenticate-panel").collapse('toggle'); 94 | }, 95 | render: function() { 96 | return ( 97 | React.createElement("div", {className: "col-xs-12 col-sm-2 text-center"}, 98 | React.createElement("button", {className: "login-btn btn btn-default", "data-toggle": "collapse", onClick: this.togglePanel, 99 | "aria-expanded": "false", "aria-controls": "authenticate-panel"}, 100 | React.createElement("span", {className: "glyphicon glyphicon-lock"}), 101 | "Sign Up or Log In" 102 | ) 103 | ) 104 | ) 105 | } 106 | }); 107 | 108 | var AccountButton = React.createClass({displayName: "AccountButton", 109 | render: function() { 110 | return ( 111 | React.createElement("a", {className: "btn btn-default account-btn", href: "/account"}, 112 | React.createElement("span", {className: "glyphicon glyphicon-user"}), 113 | "Account" 114 | ) 115 | ); 116 | } 117 | }); 118 | 119 | var LogoutButton = React.createClass({displayName: "LogoutButton", 120 | render: function() { 121 | return ( 122 | React.createElement("a", {className: "btn btn-default logout-btn", href: "/logout"}, 123 | React.createElement("span", {className: "glyphicon glyphicon-off"}), 124 | "Log out" 125 | ) 126 | ); 127 | } 128 | }); 129 | 130 | var AuthenticatePanel = React.createClass({displayName: "AuthenticatePanel", 131 | toggleFAQ: function() { 132 | $("#login-faq").collapse("toggle"); 133 | }, 134 | authenticate: function() { 135 | window.location.href = "/login?returnURL=" + encodeURIComponent(window.location.pathname + window.location.search); 136 | }, 137 | render: function() { 138 | return ( 139 | React.createElement("div", {className: "row collapse", id: "authenticate-panel"}, 140 | React.createElement("div", {className: "col-xs-12 text-center"}, 141 | React.createElement("h2", null, React.createElement("strong", null, "Let's get you authenticated."), React.createElement("span", {className: "glyphicon glyphicon-question-sign", onClick: this.toggleFAQ})), 142 | React.createElement("div", {id: "login-faq", className: "container collapse"}, 143 | React.createElement("div", {className: "login-faq-question"}, React.createElement("strong", null, "Why should I?")), 144 | React.createElement("div", {className: "login-faq-answer"}, "Authentication helps prevent malicious users from submitting bad or duplicate referral codes and prevents robots from taking over the site."), 145 | React.createElement("div", {className: "login-faq-question"}, React.createElement("strong", null, "Why Google?")), 146 | React.createElement("div", {className: "login-faq-answer"}, "Google has a respectable history of protecting user passwords. Authenticating with Google means that Refer Madness will never see your password. It also means one less password for you to remember (and, eventually, forget)."), 147 | React.createElement("div", {className: "login-faq-question"}, React.createElement("strong", null, "Where's the legal information?")), 148 | React.createElement("div", {className: "login-faq-answer"}, "You can view the privacy policy and terms of service on ", React.createElement("a", {href: "/legal"}, "the legal page"), ".") 149 | ), 150 | React.createElement("button", {className: "btn btn-default btn-lg btn-google", onClick: this.authenticate}, 151 | React.createElement("span", {className: "glyphicon google-plus"}), 152 | "Sign in with Google" 153 | ), 154 | React.createElement("h5", null, "By signing in using the link above, you agree to the ", React.createElement("a", {href: "/legal"}, "Terms and Conditions"), ".") 155 | ) 156 | ) 157 | ); 158 | } 159 | }); 160 | 161 | var Header = React.createClass({displayName: "Header", 162 | getInitialState: function() { 163 | return { 164 | loggedIn: $("body").attr("data-logged-in") === "true" 165 | } 166 | }, 167 | render: function() { 168 | if (this.state.loggedIn) { 169 | return ( 170 | React.createElement("div", {className: "header"}, 171 | React.createElement("div", {className: "container-fluid"}, 172 | React.createElement("div", {className: "row"}, 173 | React.createElement(TitleArea, {smallTitle: this.props.smallTitle}), 174 | React.createElement("div", {className: "text-center"}, 175 | React.createElement(AccountButton, null), 176 | React.createElement(LogoutButton, null) 177 | ) 178 | ) 179 | ) 180 | ) 181 | ) 182 | } else { 183 | return ( 184 | React.createElement("div", {className: "header"}, 185 | React.createElement("div", {className: "container-fluid"}, 186 | React.createElement("div", {className: "row"}, 187 | React.createElement(TitleArea, {smallTitle: this.props.smallTitle}), 188 | React.createElement(LoginButton, null) 189 | ), 190 | React.createElement(AuthenticatePanel, null) 191 | ) 192 | ) 193 | ) 194 | } 195 | } 196 | }); -------------------------------------------------------------------------------- /public/scripts/search.jsx: -------------------------------------------------------------------------------- 1 | var CreateResult = React.createClass({ 2 | create: function() { 3 | $(".search-box").addClass("fade-out"); 4 | var that = this; 5 | $(".search-result").each(function(i, item) { 6 | setTimeout(function() { 7 | $(item).addClass("fade-out"); 8 | }, (i+1)*200); 9 | }); 10 | setTimeout(function() { 11 | that.props.onCreate(); 12 | }, ($(".search-result").length+1)*200); 13 | }, 14 | render: function() { 15 | return ( 16 | <div className="search-result create-search-result col-md-3-point-5 col-sm-6 col-xs-12 hidden" onClick={this.create}> 17 | <div className="row"> 18 | <div className="col-xs-offset-1 col-xs-3"> 19 | <span className="glyphicon glyphicon-plus"></span> 20 | </div> 21 | <div className="col-xs-7"> 22 | <h2> 23 | Add 24 | </h2> 25 | <h2> 26 | New 27 | </h2> 28 | </div> 29 | </div> 30 | </div> 31 | ); 32 | } 33 | }); 34 | 35 | var SearchResults = React.createClass({ 36 | selectResult: function(data) { 37 | this.props.onResultSelected(data) 38 | }, 39 | standardizeResultHeights: function() { 40 | var results = $(".search-result").height("inherit"); 41 | if (results.length > 1) { 42 | var standardHeight = Math.max.apply(null, 43 | results.map(function(idx, el) { 44 | return $(el).height(); 45 | }).get()); 46 | results.each(function() { 47 | $(this).height(standardHeight); 48 | }); 49 | } 50 | }, 51 | componentDidMount: function() { 52 | if (window.location.pathname !== "/") { 53 | $(".create-search-result").removeClass("hidden"); 54 | } 55 | this.standardizeResultHeights(); 56 | }, 57 | componentDidUpdate: function() { 58 | $(".create-search-result").removeClass("hidden"); 59 | this.standardizeResultHeights(); 60 | }, 61 | newService: function() { 62 | this.props.onNewService(); 63 | }, 64 | render: function() { 65 | var that = this; 66 | var results = this.props.data.map(function (result) { 67 | return ( 68 | <Result key={result.ID} data={result} onSelected={that.selectResult} /> 69 | ); 70 | }); 71 | 72 | return ( 73 | <div className="search-results row"> 74 | {results} 75 | <CreateResult onCreate={this.newService} /> 76 | </div> 77 | ); 78 | } 79 | }); 80 | 81 | var SearchBox = React.createClass({ 82 | onTextChange: function(e) { 83 | this.props.onSearchTextChange(React.findDOMNode(this.refs.text).value); 84 | }, 85 | edit: function(e) { 86 | var currentSearch = React.findDOMNode(this.refs.text).value; 87 | this.props.onSearchTextChange(currentSearch); 88 | }, 89 | componentDidMount: function() { 90 | if (this.props.isReadonly !== true) { 91 | $(".search-box input").select(); 92 | } 93 | }, 94 | render: function() { 95 | if (this.props.isReadonly !== true) { 96 | return ( 97 | <div className="search-box"> 98 | <input type="text" onChange={$.debounce(300, this.onTextChange)} className="form-control input-lg" ref="text" 99 | placeholder="Give me a service name or URL!" defaultValue={this.props.initialSearch} /> 100 | </div> 101 | ); 102 | } else { 103 | return ( 104 | <div className="search-box"> 105 | <input type="text" onChange={$.debounce(300, this.onTextChange)} onClick={this.edit} className="form-control input-lg disabled" ref="text" 106 | placeholder="Give me a service name or URL!" defaultValue={this.props.initialSearch} /> 107 | </div> 108 | ); 109 | } 110 | } 111 | }); 112 | 113 | var SearchPage = React.createClass({ 114 | getSearchParam: function() { 115 | var search = window.location.search.substring(1).split("&"); 116 | var searchMap = {}; 117 | search.forEach(function(item) { 118 | var splitVals = item.split("="); 119 | if (splitVals.length != 2) { 120 | return; 121 | } 122 | searchMap[splitVals[0]] = splitVals[1]; 123 | }); 124 | 125 | return searchMap["q"] ? decodeURIComponent(searchMap["q"]) : ""; 126 | }, 127 | getInitialState: function() { 128 | var query = this.getSearchParam(); 129 | var data = []; 130 | if ($("#content").attr("data-search-results")) { 131 | data = JSON.parse($("#content").attr("data-search-results")); 132 | } 133 | return { 134 | services: data.Services || [], 135 | total: 0, 136 | selected: this.props.selected || -1, 137 | creating: this.props.creating, 138 | initialSearch: query 139 | }; 140 | }, 141 | getFilteredData: function(query) { 142 | query = encodeURIComponent($.trim(query)); 143 | 144 | var that = this; 145 | $.ajax({ 146 | url: "/search?q=" + query + "&skip=0&limit=11", 147 | method: "POST", 148 | contentType: "application/json", 149 | success: function(data) { 150 | history.pushState(null, null, "/search?q=" + query); 151 | that.setState({services: data.Services || [], total: data.Total}); 152 | }, 153 | error: function(xhr) { 154 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 155 | } 156 | }); 157 | }, 158 | getMoreResults: function() { 159 | var that = this; 160 | $.ajax({ 161 | url: "/search?q=" + that.state.initialSearch + "&skip=" + that.state.services.length + "&limit=11", 162 | method: "POST", 163 | contentType: "application/json", 164 | success: function(data) { 165 | history.pushState(null, null, "/search?q=" + query); 166 | that.setState({services: that.state.services.concat(data.Services || []), total: data.Total}); 167 | }, 168 | error: function(xhr) { 169 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 170 | } 171 | }); 172 | }, 173 | handleSearchTextChange: function(query) { 174 | this.getFilteredData(query); 175 | if (this.props.onNonEmptySearch) { 176 | this.props.onNonEmptySearch(); 177 | } 178 | this.setState({initialSearch: query, selected: -1}); 179 | }, 180 | resultSelected: function(data) { 181 | var animationFinished = false, endAnimation = $(".search-result").length-1; 182 | $(".search-result").each(function(i, item) { 183 | setTimeout(function() { 184 | $(item).addClass("fade-out"); 185 | if (i === endAnimation) { 186 | animationFinished = true; 187 | } 188 | }, i*200); 189 | }); 190 | 191 | var that = this; 192 | $.ajax({ 193 | url: "/service/" + data.ID, 194 | contentType: "application/json", 195 | success: function(service) { 196 | var proceedToServicePage = function() { 197 | setTimeout(function() { 198 | if (animationFinished) { 199 | history.pushState(null, null, "/service/" + service.ID + "?q=" + encodeURIComponent($(".search-box input").val())); 200 | that.setState({selected: service}); 201 | } else { 202 | proceedToServicePage(); 203 | } 204 | }, 100); 205 | }; 206 | proceedToServicePage(); 207 | }, 208 | error: function(xhr) { 209 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 210 | } 211 | }); 212 | }, 213 | createService: function() { 214 | var searchText = encodeURIComponent($(React.findDOMNode(this.refs.searchbox)).find("input").val()); 215 | history.pushState(null, null, "/search?q=" + searchText); 216 | history.pushState(null, null, "/service/create"); 217 | this.setState({creating: true}); 218 | }, 219 | handleServiceCreated: function(service) { 220 | history.pushState(null, null, "/service/" + service.ID); 221 | this.setState({creating: false, selected: service}); 222 | }, 223 | render: function() { 224 | if (this.state.creating) { 225 | return ( 226 | <div className="search-area"> 227 | <CreateService fadeIn={this.props.originalTarget !== "create-service"} onCreated={this.handleServiceCreated} /> 228 | </div> 229 | ); 230 | } else if (this.state.selected === -1) { 231 | return ( 232 | <div className="search-area"> 233 | <SearchBox onSearchTextChange={this.handleSearchTextChange} ref="searchbox" initialSearch={this.state.initialSearch}/> 234 | <SearchResults data={this.state.services} onResultSelected={this.resultSelected} onNewService={this.createService} /> 235 | <MoreResults isVisible={this.state.total > this.state.services.length} onMore={this.getMoreResults} /> 236 | </div> 237 | ); 238 | } else { 239 | var searchText = this.state.initialSearch || this.getSearchParam() || this.state.selected.Name 240 | return ( 241 | <div className="search-area"> 242 | <SearchBox onSearchTextChange={this.handleSearchTextChange} ref="searchbox" isReadonly={true} 243 | initialSearch={this.state.initialSearch || this.state.selected.Name}/> 244 | <ServicePage data={this.state.selected} /> 245 | </div> 246 | ) 247 | } 248 | } 249 | }); 250 | -------------------------------------------------------------------------------- /public/scripts/search.js: -------------------------------------------------------------------------------- 1 | var CreateResult = React.createClass({displayName: "CreateResult", 2 | create: function() { 3 | $(".search-box").addClass("fade-out"); 4 | var that = this; 5 | $(".search-result").each(function(i, item) { 6 | setTimeout(function() { 7 | $(item).addClass("fade-out"); 8 | }, (i+1)*200); 9 | }); 10 | setTimeout(function() { 11 | that.props.onCreate(); 12 | }, ($(".search-result").length+1)*200); 13 | }, 14 | render: function() { 15 | return ( 16 | React.createElement("div", {className: "search-result create-search-result col-md-3-point-5 col-sm-6 col-xs-12 hidden", onClick: this.create}, 17 | React.createElement("div", {className: "row"}, 18 | React.createElement("div", {className: "col-xs-offset-1 col-xs-3"}, 19 | React.createElement("span", {className: "glyphicon glyphicon-plus"}) 20 | ), 21 | React.createElement("div", {className: "col-xs-7"}, 22 | React.createElement("h2", null, 23 | "Add" 24 | ), 25 | React.createElement("h2", null, 26 | "New" 27 | ) 28 | ) 29 | ) 30 | ) 31 | ); 32 | } 33 | }); 34 | 35 | var SearchResults = React.createClass({displayName: "SearchResults", 36 | selectResult: function(data) { 37 | this.props.onResultSelected(data) 38 | }, 39 | standardizeResultHeights: function() { 40 | var results = $(".search-result").height("inherit"); 41 | if (results.length > 1) { 42 | var standardHeight = Math.max.apply(null, 43 | results.map(function(idx, el) { 44 | return $(el).height(); 45 | }).get()); 46 | results.each(function() { 47 | $(this).height(standardHeight); 48 | }); 49 | } 50 | }, 51 | componentDidMount: function() { 52 | if (window.location.pathname !== "/") { 53 | $(".create-search-result").removeClass("hidden"); 54 | } 55 | this.standardizeResultHeights(); 56 | }, 57 | componentDidUpdate: function() { 58 | $(".create-search-result").removeClass("hidden"); 59 | this.standardizeResultHeights(); 60 | }, 61 | newService: function() { 62 | this.props.onNewService(); 63 | }, 64 | render: function() { 65 | var that = this; 66 | var results = this.props.data.map(function (result) { 67 | return ( 68 | React.createElement(Result, {key: result.ID, data: result, onSelected: that.selectResult}) 69 | ); 70 | }); 71 | 72 | return ( 73 | React.createElement("div", {className: "search-results row"}, 74 | results, 75 | React.createElement(CreateResult, {onCreate: this.newService}) 76 | ) 77 | ); 78 | } 79 | }); 80 | 81 | var SearchBox = React.createClass({displayName: "SearchBox", 82 | onTextChange: function(e) { 83 | this.props.onSearchTextChange(React.findDOMNode(this.refs.text).value); 84 | }, 85 | edit: function(e) { 86 | var currentSearch = React.findDOMNode(this.refs.text).value; 87 | this.props.onSearchTextChange(currentSearch); 88 | }, 89 | componentDidMount: function() { 90 | if (this.props.isReadonly !== true) { 91 | $(".search-box input").select(); 92 | } 93 | }, 94 | render: function() { 95 | if (this.props.isReadonly !== true) { 96 | return ( 97 | React.createElement("div", {className: "search-box"}, 98 | React.createElement("input", {type: "text", onChange: $.debounce(300, this.onTextChange), className: "form-control input-lg", ref: "text", 99 | placeholder: "Give me a service name or URL!", defaultValue: this.props.initialSearch}) 100 | ) 101 | ); 102 | } else { 103 | return ( 104 | React.createElement("div", {className: "search-box"}, 105 | React.createElement("input", {type: "text", onChange: $.debounce(300, this.onTextChange), onClick: this.edit, className: "form-control input-lg disabled", ref: "text", 106 | placeholder: "Give me a service name or URL!", defaultValue: this.props.initialSearch}) 107 | ) 108 | ); 109 | } 110 | } 111 | }); 112 | 113 | var SearchPage = React.createClass({displayName: "SearchPage", 114 | getSearchParam: function() { 115 | var search = window.location.search.substring(1).split("&"); 116 | var searchMap = {}; 117 | search.forEach(function(item) { 118 | var splitVals = item.split("="); 119 | if (splitVals.length != 2) { 120 | return; 121 | } 122 | searchMap[splitVals[0]] = splitVals[1]; 123 | }); 124 | 125 | return searchMap["q"] ? decodeURIComponent(searchMap["q"]) : ""; 126 | }, 127 | getInitialState: function() { 128 | var query = this.getSearchParam(); 129 | var data = []; 130 | if ($("#content").attr("data-search-results")) { 131 | data = JSON.parse($("#content").attr("data-search-results")); 132 | } 133 | return { 134 | services: data.Services || [], 135 | total: 0, 136 | selected: this.props.selected || -1, 137 | creating: this.props.creating, 138 | initialSearch: query 139 | }; 140 | }, 141 | getFilteredData: function(query) { 142 | query = encodeURIComponent($.trim(query)); 143 | 144 | var that = this; 145 | $.ajax({ 146 | url: "/search?q=" + query + "&skip=0&limit=11", 147 | method: "POST", 148 | contentType: "application/json", 149 | success: function(data) { 150 | history.pushState(null, null, "/search?q=" + query); 151 | that.setState({services: data.Services || [], total: data.Total}); 152 | }, 153 | error: function(xhr) { 154 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 155 | } 156 | }); 157 | }, 158 | getMoreResults: function() { 159 | var that = this; 160 | $.ajax({ 161 | url: "/search?q=" + that.state.initialSearch + "&skip=" + that.state.services.length + "&limit=11", 162 | method: "POST", 163 | contentType: "application/json", 164 | success: function(data) { 165 | history.pushState(null, null, "/search?q=" + query); 166 | that.setState({services: that.state.services.concat(data.Services || []), total: data.Total}); 167 | }, 168 | error: function(xhr) { 169 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 170 | } 171 | }); 172 | }, 173 | handleSearchTextChange: function(query) { 174 | this.getFilteredData(query); 175 | if (this.props.onNonEmptySearch) { 176 | this.props.onNonEmptySearch(); 177 | } 178 | this.setState({initialSearch: query, selected: -1}); 179 | }, 180 | resultSelected: function(data) { 181 | var animationFinished = false, endAnimation = $(".search-result").length-1; 182 | $(".search-result").each(function(i, item) { 183 | setTimeout(function() { 184 | $(item).addClass("fade-out"); 185 | if (i === endAnimation) { 186 | animationFinished = true; 187 | } 188 | }, i*200); 189 | }); 190 | 191 | var that = this; 192 | $.ajax({ 193 | url: "/service/" + data.ID, 194 | contentType: "application/json", 195 | success: function(service) { 196 | var proceedToServicePage = function() { 197 | setTimeout(function() { 198 | if (animationFinished) { 199 | history.pushState(null, null, "/service/" + service.ID + "?q=" + encodeURIComponent($(".search-box input").val())); 200 | that.setState({selected: service}); 201 | } else { 202 | proceedToServicePage(); 203 | } 204 | }, 100); 205 | }; 206 | proceedToServicePage(); 207 | }, 208 | error: function(xhr) { 209 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 210 | } 211 | }); 212 | }, 213 | createService: function() { 214 | var searchText = encodeURIComponent($(React.findDOMNode(this.refs.searchbox)).find("input").val()); 215 | history.pushState(null, null, "/search?q=" + searchText); 216 | history.pushState(null, null, "/service/create"); 217 | this.setState({creating: true}); 218 | }, 219 | handleServiceCreated: function(service) { 220 | history.pushState(null, null, "/service/" + service.ID); 221 | this.setState({creating: false, selected: service}); 222 | }, 223 | render: function() { 224 | if (this.state.creating) { 225 | return ( 226 | React.createElement("div", {className: "search-area"}, 227 | React.createElement(CreateService, {fadeIn: this.props.originalTarget !== "create-service", onCreated: this.handleServiceCreated}) 228 | ) 229 | ); 230 | } else if (this.state.selected === -1) { 231 | return ( 232 | React.createElement("div", {className: "search-area"}, 233 | React.createElement(SearchBox, {onSearchTextChange: this.handleSearchTextChange, ref: "searchbox", initialSearch: this.state.initialSearch}), 234 | React.createElement(SearchResults, {data: this.state.services, onResultSelected: this.resultSelected, onNewService: this.createService}), 235 | React.createElement(MoreResults, {isVisible: this.state.total > this.state.services.length, onMore: this.getMoreResults}) 236 | ) 237 | ); 238 | } else { 239 | var searchText = this.state.initialSearch || this.getSearchParam() || this.state.selected.Name 240 | return ( 241 | React.createElement("div", {className: "search-area"}, 242 | React.createElement(SearchBox, {onSearchTextChange: this.handleSearchTextChange, ref: "searchbox", isReadonly: true, 243 | initialSearch: this.state.initialSearch || this.state.selected.Name}), 244 | React.createElement(ServicePage, {data: this.state.selected}) 245 | ) 246 | ) 247 | } 248 | } 249 | }); 250 | -------------------------------------------------------------------------------- /public/scripts/landing-home.jsx: -------------------------------------------------------------------------------- 1 | var SearchPanel = React.createClass({ 2 | handleSearchActivated: function() { 3 | if (this.props.onSearchActivated) { 4 | this.props.onSearchActivated(); 5 | $(".search-panel-message").addClass("fadeout"); 6 | $(".title").addClass("shrink"); 7 | history.pushState(null, null, "/search"); 8 | } 9 | }, 10 | render: function() { 11 | return ( 12 | <div className="search-panel"> 13 | <Header /> 14 | <div className="container text-center"> 15 | <h1 className="search-panel-message"><strong>Looking for referral links?</strong></h1> 16 | <h2 className="search-panel-message"><strong>Start searching below to find your product or service.</strong></h2> 17 | <SearchPage onNonEmptySearch={this.handleSearchActivated} /> 18 | </div> 19 | </div> 20 | ); 21 | } 22 | }); 23 | 24 | var LonelyPanel = React.createClass({ 25 | render: function() { 26 | return ( 27 | <div className="lonely-panel"> 28 | <div className="container"> 29 | <div className="row"> 30 | <div className="col-md-4 col-xs-12"> 31 | <div className="row"> 32 | <div className="col-xs-12 text-center"> 33 | <h1>No</h1><h2>friends?</h2> 34 | </div> 35 | </div> 36 | <div className="row"> 37 | <div className="col-xs-12"> 38 | <div id="no-friends" /> 39 | </div> 40 | </div> 41 | </div> 42 | <div className="col-md-4 col-xs-12"> 43 | <div className="row"> 44 | <div className="col-xs-12 text-center"> 45 | <h1>No</h1><h2>family?</h2> 46 | </div> 47 | </div> 48 | <div className="row"> 49 | <div className="col-xs-12"> 50 | <div id="no-family" /> 51 | </div> 52 | </div> 53 | </div> 54 | <div className="col-md-4 col-xs-12"> 55 | <div className="row"> 56 | <div className="col-xs-12 text-center"> 57 | <h1>No</h1><h2>followers?</h2> 58 | </div> 59 | </div> 60 | <div className="row"> 61 | <div className="col-xs-12"> 62 | <div id="no-followers" /> 63 | </div> 64 | </div> 65 | </div> 66 | </div> 67 | <div className="row" id="no-problem"> 68 | <div className="col-xs-12"> 69 | <div className="row"> 70 | <div className="col-xs-12 text-center"><h1>No problem.</h1></div> 71 | </div> 72 | <div className="row"> 73 | <div className="col-xs-12 text-center"> 74 | <img src="/img/no-problem.png" /> 75 | </div> 76 | </div> 77 | </div> 78 | </div> 79 | </div> 80 | </div> 81 | ); 82 | } 83 | }); 84 | 85 | var HookPanel = React.createClass({ 86 | render: function() { 87 | return ( 88 | <div className="hook-panel"> 89 | <div className="container"> 90 | <div className="row"> 91 | <div className="col-xs-12 text-center"> 92 | <h1>Find a random referral code to get mutual discounts</h1> 93 | </div> 94 | </div> 95 | <div className="row"> 96 | <div className="col-xs-12 text-center"> 97 | <img width="300px" src="/img/helping-hands.png" alt="Friends with benefits" /> 98 | </div> 99 | </div> 100 | <div className="row"> 101 | <div className="col-xs-12 text-center"> 102 | <h1>Then submit your own for others to use</h1> 103 | </div> 104 | </div> 105 | </div> 106 | </div> 107 | ) 108 | } 109 | }); 110 | 111 | var PopularPanel = React.createClass({ 112 | selectResult: function(data) { 113 | window.location.href = "/service/" + data.ID; 114 | }, 115 | standardizeResultHeights: function() { 116 | var results = $(".popular-panel .search-result"); 117 | var standardHeight = Math.max.apply(null, 118 | results.map(function(idx, el) { 119 | return $(el).height(); 120 | }).get()); 121 | results.each(function() { 122 | $(this).height(standardHeight); 123 | }); 124 | }, 125 | componentDidMount: function() { 126 | this.standardizeResultHeights(); 127 | }, 128 | componentDidUpdate: function() { 129 | this.standardizeResultHeights(); 130 | }, 131 | fetchData: function() { 132 | var that = this; 133 | $.ajax({ 134 | url: "/service/popular", 135 | contentType: "application/json", 136 | success: function(data) { 137 | that.setState({services: data || []}); 138 | } 139 | }); 140 | }, 141 | getInitialState: function() { 142 | this.fetchData(); 143 | return { 144 | services: [] 145 | }; 146 | }, 147 | render: function() { 148 | var that = this; 149 | var results = this.state.services.map(function (result) { 150 | return ( 151 | <Result key={result.ID} data={result} onSelected={that.selectResult} /> 152 | ); 153 | }); 154 | 155 | return ( 156 | <div className="popular-panel"> 157 | <div className="container"> 158 | <div className="row"> 159 | <div className="col-xs-12 col-md-3 text-center"> 160 | <h1>Most</h1> 161 | <h1>Popular</h1> 162 | </div> 163 | {results} 164 | </div> 165 | </div> 166 | </div> 167 | ); 168 | } 169 | }); 170 | 171 | var RecentPanel = React.createClass({ 172 | selectResult: function(data) { 173 | window.location.href = "/service/" + data.ID; 174 | }, 175 | standardizeResultHeights: function() { 176 | var results = $(".recent-panel .search-result"); 177 | var standardHeight = Math.max.apply(null, 178 | results.map(function(idx, el) { 179 | return $(el).height(); 180 | }).get()); 181 | results.each(function() { 182 | $(this).height(standardHeight); 183 | }); 184 | }, 185 | componentDidMount: function() { 186 | this.standardizeResultHeights(); 187 | }, 188 | componentDidUpdate: function() { 189 | this.standardizeResultHeights(); 190 | }, 191 | fetchData: function() { 192 | var that = this; 193 | $.ajax({ 194 | url: "/service/recent", 195 | contentType: "application/json", 196 | success: function(data) { 197 | that.setState({services: data || []}); 198 | } 199 | }); 200 | }, 201 | getInitialState: function() { 202 | this.fetchData(); 203 | return { 204 | services: [] 205 | }; 206 | }, 207 | render: function() { 208 | var that = this; 209 | var results = this.state.services.map(function (result) { 210 | return ( 211 | <Result key={result.ID} data={result} onSelected={that.selectResult} /> 212 | ); 213 | }); 214 | 215 | return ( 216 | <div className="recent-panel"> 217 | <div className="container"> 218 | <div className="row"> 219 | <div className="col-xs-12 col-md-3 text-center"> 220 | <h1>Most</h1> 221 | <h1>Recent</h1> 222 | </div> 223 | {results} 224 | </div> 225 | </div> 226 | </div> 227 | ); 228 | } 229 | }); 230 | 231 | var GetStartedPanel = React.createClass({ 232 | focusOnSearch: function() { 233 | $(".search-panel")[0].scrollIntoView(); 234 | $(".search-box input").focus(); 235 | }, 236 | render: function() { 237 | return ( 238 | <div className="get-started-panel"> 239 | <div className="container"> 240 | <div className="row"> 241 | <div className="col-xs-12 text-center"> 242 | <h1> 243 | Why haven't you started yet? 244 | </h1> 245 | </div> 246 | </div> 247 | <div className="row"> 248 | <div className="col-xs-12 text-center"> 249 | <h1> 250 | It's <em>literally <strong>free</strong></em>. 251 | </h1> 252 | </div> 253 | </div> 254 | <div className="row"> 255 | <div className="col-xs-12 text-center"> 256 | <h1> 257 | You could even <strong>make</strong> money. 258 | </h1> 259 | </div> 260 | </div> 261 | <div className="row"> 262 | <div className="col-xs-12 text-center"> 263 | <a className="btn btn-default" href="javascript:void(0)" onClick={this.focusOnSearch}> 264 | <span className="glyphicon glyphicon-search" /> 265 | Go ahead - search for a service 266 | </a> 267 | </div> 268 | </div> 269 | </div> 270 | </div> 271 | ); 272 | } 273 | }); 274 | 275 | var LandingHome = React.createClass({ 276 | getInitialState: function() { 277 | var waitToPop = /^((?!chrome).)*safari/i.test(navigator.userAgent); 278 | $(window).off("popstate").on("popstate", function() { 279 | if (waitToPop) { 280 | waitToPop = false; 281 | return; 282 | } 283 | window.location = window.location.href; 284 | }); 285 | 286 | return {searchActive: false}; 287 | }, 288 | handleSearchActivated: function() { 289 | this.setState({searchActive: true}); 290 | }, 291 | render: function() { 292 | if (this.state.searchActive) { 293 | return ( 294 | <div className="home-page"> 295 | <SearchPanel /> 296 | </div> 297 | ); 298 | } else { 299 | return ( 300 | <div className="home-page"> 301 | <SearchPanel onSearchActivated={this.handleSearchActivated} /> 302 | <LonelyPanel /> 303 | <HookPanel /> 304 | <PopularPanel /> 305 | <RecentPanel /> 306 | <GetStartedPanel /> 307 | </div> 308 | ); 309 | } 310 | } 311 | }); 312 | 313 | React.render( 314 | <LandingHome />, 315 | document.getElementById('content') 316 | ); -------------------------------------------------------------------------------- /public/scripts/account.jsx: -------------------------------------------------------------------------------- 1 | var UserReferralCodes = React.createClass({ 2 | getInitialState: function() { 3 | this.fetchServices(); 4 | return { 5 | services: [], 6 | total: 0 7 | }; 8 | }, 9 | fetchServices: function(limit) { 10 | var that = this, 11 | skip = this.state ? this.state.services.length : 0; 12 | $.ajax({ 13 | url: "/account/services?limit=11&skip=" + skip, 14 | method: "GET", 15 | contentType: "application/json", 16 | success: function(data) { 17 | that.setState({services: that.state.services.concat(data.Services || []), total: data.Total}); 18 | }, 19 | error: function(xhr) { 20 | console.log("Error fetching user services", xhr) 21 | } 22 | }); 23 | }, 24 | standardizeResultHeights: function() { 25 | var results = $(".search-result"); 26 | if (results.length > 1) { 27 | var standardHeight = Math.max.apply(null, 28 | results.map(function(idx, el) { 29 | return $(el).height(); 30 | }).get()); 31 | results.each(function() { 32 | $(this).height(standardHeight); 33 | }); 34 | } 35 | }, 36 | componentDidUpdate: function() { 37 | this.standardizeResultHeights(); 38 | }, 39 | viewService: function(service) { 40 | window.location.href = "/service/" + service.ID; 41 | }, 42 | render: function() { 43 | if (this.state.services.length > 0) { 44 | var that = this; 45 | var services = this.state.services.map(function (service) { 46 | return ( 47 | <Result key={service.ID} data={service} onSelected={that.viewService} /> 48 | ); 49 | }); 50 | return ( 51 | <div className="user-referral-codes container"> 52 | <h2 className="text-center">Your Services</h2> 53 | <div className="row"> 54 | {services} 55 | </div> 56 | <MoreResults isVisible={this.state.total > this.state.services.length} onMore={this.fetchServices} /> 57 | </div> 58 | ); 59 | } else { 60 | return null; 61 | } 62 | } 63 | }); 64 | 65 | var SwitchAccounts = React.createClass({ 66 | getInitialState: function() { 67 | return { 68 | waitForConfirmation: false 69 | } 70 | }, 71 | switchAccounts: function() { 72 | $(".switch-account-information").addClass("fade-out"); 73 | var that = this; 74 | setTimeout(function() { 75 | that.setState({waitForConfirmation: true}) 76 | }, 300); 77 | }, 78 | componentDidUpdate: function() { 79 | setTimeout(function() { 80 | $(".switch-account-information").removeClass("fade-out"); 81 | }); 82 | }, 83 | redirect: function() { 84 | window.location.href = "/account/switch"; 85 | }, 86 | cancel: function() { 87 | $(".switch-account-information").addClass("fade-out"); 88 | var that = this; 89 | setTimeout(function() { 90 | that.setState({waitForConfirmation: false}) 91 | }, 300); 92 | }, 93 | render: function () { 94 | if (!this.state.waitForConfirmation) { 95 | return ( 96 | <div className="row"> 97 | <div className="col-xs-12 text-center switch-account-information"> 98 | <button className="btn btn-default btn-lg switch-accounts" onClick={this.switchAccounts}> 99 | <span className="glyphicon glyphicon-transfer"></span> 100 | Use Different Google Identity 101 | </button> 102 | </div> 103 | </div> 104 | ); 105 | } else { 106 | return ( 107 | <div className="row"> 108 | <div className="col-xs-12 text-center switch-account-information"> 109 | <span className="switch-account-confirmation">Change which Google identity you use to authenticate?</span> 110 | <button className="btn btn-default btn-lg btn-google switch-accounts" onClick={this.redirect}> 111 | <span className="glyphicon google-plus"></span> 112 | Yup, take me to Google 113 | </button> 114 | <button className="btn btn-default btn-lg switch-accounts-cancel" onClick={this.cancel}> 115 | <span className="glyphicon glyphicon glyphicon-ban-circle"></span> 116 | Nevermind 117 | </button> 118 | </div> 119 | </div> 120 | ); 121 | } 122 | } 123 | }); 124 | 125 | var CancelAccountDeletion = React.createClass({ 126 | render: function() { 127 | return ( 128 | <button className="btn btn-default btn-lg cancel-account-deletion" onClick={this.props.onClick}> 129 | <span className="glyphicon glyphicon glyphicon-ban-circle"></span> 130 | Cancel 131 | </button> 132 | ); 133 | } 134 | }); 135 | 136 | var VerifyAccountDeletionDesparation = React.createClass({ 137 | render: function() { 138 | return ( 139 | <div className="desperate-delete-message collapse text-center"> 140 | <h3> 141 | Wait! Don't go! I never got the chance to tell you, but... <strong>I love you!</strong> 142 | </h3> 143 | <button className="btn btn-danger btn-lg" onClick={this.props.onContinue}> 144 | <span className="glyphicon glyphicon-heart-empty"></span> 145 | Sorry, pal, but the feeling's not mutual 146 | </button> 147 | <CancelAccountDeletion onClick={this.props.onCancel} /> 148 | </div> 149 | ); 150 | } 151 | }); 152 | 153 | var VerifyAccountDeletionApology = React.createClass({ 154 | render: function() { 155 | return ( 156 | <div className="apologetic-delete-message collapse text-center"> 157 | <h4> 158 | ...Er. Sorry about that. Overreaction on my part! <em>Please don't tell my supervisor.</em> 159 | </h4> 160 | <button className="btn btn-danger btn-lg" onClick={this.props.onContinue}> 161 | <span className="glyphicon glyphicon-thumbs-up"></span> 162 | Sure, I can be discreet, let's get on with this 163 | </button> 164 | <CancelAccountDeletion onClick={this.props.onCancel} /> 165 | </div> 166 | ); 167 | } 168 | }); 169 | 170 | var VerifyAccountDeletionWarning = React.createClass({ 171 | validate: function() { 172 | if ($(".delete-account-validation").val() === this.props.username) { 173 | $(".warning-delete-message .btn-danger").prop("disabled", false).removeClass("disabled"); 174 | } else { 175 | $(".warning-delete-message .btn-danger").prop("disabled", true).addClass("disabled"); 176 | } 177 | }, 178 | componentDidMount: function() { 179 | this.validate(); 180 | }, 181 | render: function() { 182 | return ( 183 | <div className="warning-delete-message collapse text-center"> 184 | <h3> 185 | <strong>Continuing will <em>permanantly delete</em> your account and remove your codes from the system.</strong> 186 | </h3> 187 | <h3> 188 | If you really want to leave, please <strong>enter your Google username in the textbox below</strong>. 189 | </h3> 190 | <div className="row"> 191 | <div className="col-sm-4 col-sm-offset-4 col-xs-12"> 192 | <form onsubmit="return false;"> 193 | <div className="form-group"> 194 | <input type="text" className="form-control input-lg delete-account-validation" 195 | onChange={this.validate} placeholder="Enter your Google identity..." /> 196 | </div> 197 | </form> 198 | </div> 199 | </div> 200 | <button className="btn btn-danger btn-lg" onClick={this.props.onContinue}> 201 | <span className="glyphicon glyphicon-fire"></span> 202 | Permanently Delete Account 203 | </button> 204 | <CancelAccountDeletion onClick={this.props.onCancel} /> 205 | </div> 206 | ); 207 | } 208 | }); 209 | 210 | var DeleteAccount = React.createClass({ 211 | initiate: function() { 212 | $(".delete-account").addClass("fade-out"); 213 | $(".desperate-delete-message").collapse("show"); 214 | }, 215 | apologize: function() { 216 | $(".desperate-delete-message").collapse("hide"); 217 | $(".apologetic-delete-message").collapse("show"); 218 | }, 219 | finalWarning: function() { 220 | $(".apologetic-delete-message").collapse("hide"); 221 | $(".warning-delete-message").collapse("show"); 222 | }, 223 | confirmDelete: function() { 224 | window.location.href = "/account/delete"; 225 | }, 226 | rejectDelete: function() { 227 | $(".desperate-delete-message, .apologetic-delete-message, .warning-delete-message").collapse("hide"); 228 | $(".delete-account").removeClass("fade-out"); 229 | }, 230 | render: function () { 231 | return ( 232 | <div className="row delete-account-section"> 233 | <div className="col-xs-12 text-center"> 234 | <button className="btn btn-danger btn-lg delete-account" onClick={this.initiate}> 235 | <span className="glyphicon glyphicon-fire"></span> 236 | Delete Refer Madness Account 237 | </button> 238 | </div> 239 | <VerifyAccountDeletionDesparation onContinue={this.apologize} onCancel={this.rejectDelete} /> 240 | <VerifyAccountDeletionApology onContinue={this.finalWarning} onCancel={this.rejectDelete} /> 241 | <VerifyAccountDeletionWarning onContinue={this.confirmDelete} onCancel={this.rejectDelete} username={this.props.username} /> 242 | </div> 243 | ); 244 | } 245 | }); 246 | 247 | var LoginSettings = React.createClass({ 248 | getInitialState: function() { 249 | return { 250 | username: this.props.username 251 | } 252 | }, 253 | render: function() { 254 | return ( 255 | <div className="login-settings container"> 256 | <h2 className="text-center"> 257 | You are currently logged in as <strong>{this.state.username}</strong> 258 | </h2> 259 | <SwitchAccounts /> 260 | <DeleteAccount username={this.state.username} /> 261 | </div> 262 | ); 263 | } 264 | }); 265 | 266 | var AccountPage = React.createClass({ 267 | render: function() { 268 | return ( 269 | <div className="account-home"> 270 | <Header smallTitle={true} /> 271 | <LoginSettings username={$("body").attr("data-username")} /> 272 | <UserReferralCodes/> 273 | </div> 274 | ); 275 | } 276 | }); 277 | 278 | React.render( 279 | <AccountPage />, 280 | document.getElementById('content') 281 | ); -------------------------------------------------------------------------------- /public/scripts/account.js: -------------------------------------------------------------------------------- 1 | var UserReferralCodes = React.createClass({displayName: "UserReferralCodes", 2 | getInitialState: function() { 3 | this.fetchServices(); 4 | return { 5 | services: [], 6 | total: 0 7 | }; 8 | }, 9 | fetchServices: function(limit) { 10 | var that = this, 11 | skip = this.state ? this.state.services.length : 0; 12 | $.ajax({ 13 | url: "/account/services?limit=11&skip=" + skip, 14 | method: "GET", 15 | contentType: "application/json", 16 | success: function(data) { 17 | that.setState({services: that.state.services.concat(data.Services || []), total: data.Total}); 18 | }, 19 | error: function(xhr) { 20 | console.log("Error fetching user services", xhr) 21 | } 22 | }); 23 | }, 24 | standardizeResultHeights: function() { 25 | var results = $(".search-result"); 26 | if (results.length > 1) { 27 | var standardHeight = Math.max.apply(null, 28 | results.map(function(idx, el) { 29 | return $(el).height(); 30 | }).get()); 31 | results.each(function() { 32 | $(this).height(standardHeight); 33 | }); 34 | } 35 | }, 36 | componentDidUpdate: function() { 37 | this.standardizeResultHeights(); 38 | }, 39 | viewService: function(service) { 40 | window.location.href = "/service/" + service.ID; 41 | }, 42 | render: function() { 43 | if (this.state.services.length > 0) { 44 | var that = this; 45 | var services = this.state.services.map(function (service) { 46 | return ( 47 | React.createElement(Result, {key: service.ID, data: service, onSelected: that.viewService}) 48 | ); 49 | }); 50 | return ( 51 | React.createElement("div", {className: "user-referral-codes container"}, 52 | React.createElement("h2", {className: "text-center"}, "Your Services"), 53 | React.createElement("div", {className: "row"}, 54 | services 55 | ), 56 | React.createElement(MoreResults, {isVisible: this.state.total > this.state.services.length, onMore: this.fetchServices}) 57 | ) 58 | ); 59 | } else { 60 | return null; 61 | } 62 | } 63 | }); 64 | 65 | var SwitchAccounts = React.createClass({displayName: "SwitchAccounts", 66 | getInitialState: function() { 67 | return { 68 | waitForConfirmation: false 69 | } 70 | }, 71 | switchAccounts: function() { 72 | $(".switch-account-information").addClass("fade-out"); 73 | var that = this; 74 | setTimeout(function() { 75 | that.setState({waitForConfirmation: true}) 76 | }, 300); 77 | }, 78 | componentDidUpdate: function() { 79 | setTimeout(function() { 80 | $(".switch-account-information").removeClass("fade-out"); 81 | }); 82 | }, 83 | redirect: function() { 84 | window.location.href = "/account/switch"; 85 | }, 86 | cancel: function() { 87 | $(".switch-account-information").addClass("fade-out"); 88 | var that = this; 89 | setTimeout(function() { 90 | that.setState({waitForConfirmation: false}) 91 | }, 300); 92 | }, 93 | render: function () { 94 | if (!this.state.waitForConfirmation) { 95 | return ( 96 | React.createElement("div", {className: "row"}, 97 | React.createElement("div", {className: "col-xs-12 text-center switch-account-information"}, 98 | React.createElement("button", {className: "btn btn-default btn-lg switch-accounts", onClick: this.switchAccounts}, 99 | React.createElement("span", {className: "glyphicon glyphicon-transfer"}), 100 | "Use Different Google Identity" 101 | ) 102 | ) 103 | ) 104 | ); 105 | } else { 106 | return ( 107 | React.createElement("div", {className: "row"}, 108 | React.createElement("div", {className: "col-xs-12 text-center switch-account-information"}, 109 | React.createElement("span", {className: "switch-account-confirmation"}, "Change which Google identity you use to authenticate?"), 110 | React.createElement("button", {className: "btn btn-default btn-lg btn-google switch-accounts", onClick: this.redirect}, 111 | React.createElement("span", {className: "glyphicon google-plus"}), 112 | "Yup, take me to Google" 113 | ), 114 | React.createElement("button", {className: "btn btn-default btn-lg switch-accounts-cancel", onClick: this.cancel}, 115 | React.createElement("span", {className: "glyphicon glyphicon glyphicon-ban-circle"}), 116 | "Nevermind" 117 | ) 118 | ) 119 | ) 120 | ); 121 | } 122 | } 123 | }); 124 | 125 | var CancelAccountDeletion = React.createClass({displayName: "CancelAccountDeletion", 126 | render: function() { 127 | return ( 128 | React.createElement("button", {className: "btn btn-default btn-lg cancel-account-deletion", onClick: this.props.onClick}, 129 | React.createElement("span", {className: "glyphicon glyphicon glyphicon-ban-circle"}), 130 | "Cancel" 131 | ) 132 | ); 133 | } 134 | }); 135 | 136 | var VerifyAccountDeletionDesparation = React.createClass({displayName: "VerifyAccountDeletionDesparation", 137 | render: function() { 138 | return ( 139 | React.createElement("div", {className: "desperate-delete-message collapse text-center"}, 140 | React.createElement("h3", null, 141 | "Wait! Don't go! I never got the chance to tell you, but... ", React.createElement("strong", null, "I love you!") 142 | ), 143 | React.createElement("button", {className: "btn btn-danger btn-lg", onClick: this.props.onContinue}, 144 | React.createElement("span", {className: "glyphicon glyphicon-heart-empty"}), 145 | "Sorry, pal, but the feeling's not mutual" 146 | ), 147 | React.createElement(CancelAccountDeletion, {onClick: this.props.onCancel}) 148 | ) 149 | ); 150 | } 151 | }); 152 | 153 | var VerifyAccountDeletionApology = React.createClass({displayName: "VerifyAccountDeletionApology", 154 | render: function() { 155 | return ( 156 | React.createElement("div", {className: "apologetic-delete-message collapse text-center"}, 157 | React.createElement("h4", null, 158 | "...Er. Sorry about that. Overreaction on my part! ", React.createElement("em", null, "Please don't tell my supervisor.") 159 | ), 160 | React.createElement("button", {className: "btn btn-danger btn-lg", onClick: this.props.onContinue}, 161 | React.createElement("span", {className: "glyphicon glyphicon-thumbs-up"}), 162 | "Sure, I can be discreet, let's get on with this" 163 | ), 164 | React.createElement(CancelAccountDeletion, {onClick: this.props.onCancel}) 165 | ) 166 | ); 167 | } 168 | }); 169 | 170 | var VerifyAccountDeletionWarning = React.createClass({displayName: "VerifyAccountDeletionWarning", 171 | validate: function() { 172 | if ($(".delete-account-validation").val() === this.props.username) { 173 | $(".warning-delete-message .btn-danger").prop("disabled", false).removeClass("disabled"); 174 | } else { 175 | $(".warning-delete-message .btn-danger").prop("disabled", true).addClass("disabled"); 176 | } 177 | }, 178 | componentDidMount: function() { 179 | this.validate(); 180 | }, 181 | render: function() { 182 | return ( 183 | React.createElement("div", {className: "warning-delete-message collapse text-center"}, 184 | React.createElement("h3", null, 185 | React.createElement("strong", null, "Continuing will ", React.createElement("em", null, "permanantly delete"), " your account and remove your codes from the system.") 186 | ), 187 | React.createElement("h3", null, 188 | "If you really want to leave, please ", React.createElement("strong", null, "enter your Google username in the textbox below"), "." 189 | ), 190 | React.createElement("div", {className: "row"}, 191 | React.createElement("div", {className: "col-sm-4 col-sm-offset-4 col-xs-12"}, 192 | React.createElement("form", {onsubmit: "return false;"}, 193 | React.createElement("div", {className: "form-group"}, 194 | React.createElement("input", {type: "text", className: "form-control input-lg delete-account-validation", 195 | onChange: this.validate, placeholder: "Enter your Google identity..."}) 196 | ) 197 | ) 198 | ) 199 | ), 200 | React.createElement("button", {className: "btn btn-danger btn-lg", onClick: this.props.onContinue}, 201 | React.createElement("span", {className: "glyphicon glyphicon-fire"}), 202 | "Permanently Delete Account" 203 | ), 204 | React.createElement(CancelAccountDeletion, {onClick: this.props.onCancel}) 205 | ) 206 | ); 207 | } 208 | }); 209 | 210 | var DeleteAccount = React.createClass({displayName: "DeleteAccount", 211 | initiate: function() { 212 | $(".delete-account").addClass("fade-out"); 213 | $(".desperate-delete-message").collapse("show"); 214 | }, 215 | apologize: function() { 216 | $(".desperate-delete-message").collapse("hide"); 217 | $(".apologetic-delete-message").collapse("show"); 218 | }, 219 | finalWarning: function() { 220 | $(".apologetic-delete-message").collapse("hide"); 221 | $(".warning-delete-message").collapse("show"); 222 | }, 223 | confirmDelete: function() { 224 | window.location.href = "/account/delete"; 225 | }, 226 | rejectDelete: function() { 227 | $(".desperate-delete-message, .apologetic-delete-message, .warning-delete-message").collapse("hide"); 228 | $(".delete-account").removeClass("fade-out"); 229 | }, 230 | render: function () { 231 | return ( 232 | React.createElement("div", {className: "row delete-account-section"}, 233 | React.createElement("div", {className: "col-xs-12 text-center"}, 234 | React.createElement("button", {className: "btn btn-danger btn-lg delete-account", onClick: this.initiate}, 235 | React.createElement("span", {className: "glyphicon glyphicon-fire"}), 236 | "Delete Refer Madness Account" 237 | ) 238 | ), 239 | React.createElement(VerifyAccountDeletionDesparation, {onContinue: this.apologize, onCancel: this.rejectDelete}), 240 | React.createElement(VerifyAccountDeletionApology, {onContinue: this.finalWarning, onCancel: this.rejectDelete}), 241 | React.createElement(VerifyAccountDeletionWarning, {onContinue: this.confirmDelete, onCancel: this.rejectDelete, username: this.props.username}) 242 | ) 243 | ); 244 | } 245 | }); 246 | 247 | var LoginSettings = React.createClass({displayName: "LoginSettings", 248 | getInitialState: function() { 249 | return { 250 | username: this.props.username 251 | } 252 | }, 253 | render: function() { 254 | return ( 255 | React.createElement("div", {className: "login-settings container"}, 256 | React.createElement("h2", {className: "text-center"}, 257 | "You are currently logged in as ", React.createElement("strong", null, this.state.username) 258 | ), 259 | React.createElement(SwitchAccounts, null), 260 | React.createElement(DeleteAccount, {username: this.state.username}) 261 | ) 262 | ); 263 | } 264 | }); 265 | 266 | var AccountPage = React.createClass({displayName: "AccountPage", 267 | render: function() { 268 | return ( 269 | React.createElement("div", {className: "account-home"}, 270 | React.createElement(Header, {smallTitle: true}), 271 | React.createElement(LoginSettings, {username: $("body").attr("data-username")}), 272 | React.createElement(UserReferralCodes, null) 273 | ) 274 | ); 275 | } 276 | }); 277 | 278 | React.render( 279 | React.createElement(AccountPage, null), 280 | document.getElementById('content') 281 | ); -------------------------------------------------------------------------------- /public/scripts/landing-home.js: -------------------------------------------------------------------------------- 1 | var SearchPanel = React.createClass({displayName: "SearchPanel", 2 | handleSearchActivated: function() { 3 | if (this.props.onSearchActivated) { 4 | this.props.onSearchActivated(); 5 | $(".search-panel-message").addClass("fadeout"); 6 | $(".title").addClass("shrink"); 7 | history.pushState(null, null, "/search"); 8 | } 9 | }, 10 | render: function() { 11 | return ( 12 | React.createElement("div", {className: "search-panel"}, 13 | React.createElement(Header, null), 14 | React.createElement("div", {className: "container text-center"}, 15 | React.createElement("h1", {className: "search-panel-message"}, React.createElement("strong", null, "Looking for referral links?")), 16 | React.createElement("h2", {className: "search-panel-message"}, React.createElement("strong", null, "Start searching below to find your product or service.")), 17 | React.createElement(SearchPage, {onNonEmptySearch: this.handleSearchActivated}) 18 | ) 19 | ) 20 | ); 21 | } 22 | }); 23 | 24 | var LonelyPanel = React.createClass({displayName: "LonelyPanel", 25 | render: function() { 26 | return ( 27 | React.createElement("div", {className: "lonely-panel"}, 28 | React.createElement("div", {className: "container"}, 29 | React.createElement("div", {className: "row"}, 30 | React.createElement("div", {className: "col-md-4 col-xs-12"}, 31 | React.createElement("div", {className: "row"}, 32 | React.createElement("div", {className: "col-xs-12 text-center"}, 33 | React.createElement("h1", null, "No"), React.createElement("h2", null, "friends?") 34 | ) 35 | ), 36 | React.createElement("div", {className: "row"}, 37 | React.createElement("div", {className: "col-xs-12"}, 38 | React.createElement("div", {id: "no-friends"}) 39 | ) 40 | ) 41 | ), 42 | React.createElement("div", {className: "col-md-4 col-xs-12"}, 43 | React.createElement("div", {className: "row"}, 44 | React.createElement("div", {className: "col-xs-12 text-center"}, 45 | React.createElement("h1", null, "No"), React.createElement("h2", null, "family?") 46 | ) 47 | ), 48 | React.createElement("div", {className: "row"}, 49 | React.createElement("div", {className: "col-xs-12"}, 50 | React.createElement("div", {id: "no-family"}) 51 | ) 52 | ) 53 | ), 54 | React.createElement("div", {className: "col-md-4 col-xs-12"}, 55 | React.createElement("div", {className: "row"}, 56 | React.createElement("div", {className: "col-xs-12 text-center"}, 57 | React.createElement("h1", null, "No"), React.createElement("h2", null, "followers?") 58 | ) 59 | ), 60 | React.createElement("div", {className: "row"}, 61 | React.createElement("div", {className: "col-xs-12"}, 62 | React.createElement("div", {id: "no-followers"}) 63 | ) 64 | ) 65 | ) 66 | ), 67 | React.createElement("div", {className: "row", id: "no-problem"}, 68 | React.createElement("div", {className: "col-xs-12"}, 69 | React.createElement("div", {className: "row"}, 70 | React.createElement("div", {className: "col-xs-12 text-center"}, React.createElement("h1", null, "No problem.")) 71 | ), 72 | React.createElement("div", {className: "row"}, 73 | React.createElement("div", {className: "col-xs-12 text-center"}, 74 | React.createElement("img", {src: "/img/no-problem.png"}) 75 | ) 76 | ) 77 | ) 78 | ) 79 | ) 80 | ) 81 | ); 82 | } 83 | }); 84 | 85 | var HookPanel = React.createClass({displayName: "HookPanel", 86 | render: function() { 87 | return ( 88 | React.createElement("div", {className: "hook-panel"}, 89 | React.createElement("div", {className: "container"}, 90 | React.createElement("div", {className: "row"}, 91 | React.createElement("div", {className: "col-xs-12 text-center"}, 92 | React.createElement("h1", null, "Find a random referral code to get mutual discounts") 93 | ) 94 | ), 95 | React.createElement("div", {className: "row"}, 96 | React.createElement("div", {className: "col-xs-12 text-center"}, 97 | React.createElement("img", {width: "300px", src: "/img/helping-hands.png", alt: "Friends with benefits"}) 98 | ) 99 | ), 100 | React.createElement("div", {className: "row"}, 101 | React.createElement("div", {className: "col-xs-12 text-center"}, 102 | React.createElement("h1", null, "Then submit your own for others to use") 103 | ) 104 | ) 105 | ) 106 | ) 107 | ) 108 | } 109 | }); 110 | 111 | var PopularPanel = React.createClass({displayName: "PopularPanel", 112 | selectResult: function(data) { 113 | window.location.href = "/service/" + data.ID; 114 | }, 115 | standardizeResultHeights: function() { 116 | var results = $(".popular-panel .search-result"); 117 | var standardHeight = Math.max.apply(null, 118 | results.map(function(idx, el) { 119 | return $(el).height(); 120 | }).get()); 121 | results.each(function() { 122 | $(this).height(standardHeight); 123 | }); 124 | }, 125 | componentDidMount: function() { 126 | this.standardizeResultHeights(); 127 | }, 128 | componentDidUpdate: function() { 129 | this.standardizeResultHeights(); 130 | }, 131 | fetchData: function() { 132 | var that = this; 133 | $.ajax({ 134 | url: "/service/popular", 135 | contentType: "application/json", 136 | success: function(data) { 137 | that.setState({services: data || []}); 138 | } 139 | }); 140 | }, 141 | getInitialState: function() { 142 | this.fetchData(); 143 | return { 144 | services: [] 145 | }; 146 | }, 147 | render: function() { 148 | var that = this; 149 | var results = this.state.services.map(function (result) { 150 | return ( 151 | React.createElement(Result, {key: result.ID, data: result, onSelected: that.selectResult}) 152 | ); 153 | }); 154 | 155 | return ( 156 | React.createElement("div", {className: "popular-panel"}, 157 | React.createElement("div", {className: "container"}, 158 | React.createElement("div", {className: "row"}, 159 | React.createElement("div", {className: "col-xs-12 col-md-3 text-center"}, 160 | React.createElement("h1", null, "Most"), 161 | React.createElement("h1", null, "Popular") 162 | ), 163 | results 164 | ) 165 | ) 166 | ) 167 | ); 168 | } 169 | }); 170 | 171 | var RecentPanel = React.createClass({displayName: "RecentPanel", 172 | selectResult: function(data) { 173 | window.location.href = "/service/" + data.ID; 174 | }, 175 | standardizeResultHeights: function() { 176 | var results = $(".recent-panel .search-result"); 177 | var standardHeight = Math.max.apply(null, 178 | results.map(function(idx, el) { 179 | return $(el).height(); 180 | }).get()); 181 | results.each(function() { 182 | $(this).height(standardHeight); 183 | }); 184 | }, 185 | componentDidMount: function() { 186 | this.standardizeResultHeights(); 187 | }, 188 | componentDidUpdate: function() { 189 | this.standardizeResultHeights(); 190 | }, 191 | fetchData: function() { 192 | var that = this; 193 | $.ajax({ 194 | url: "/service/recent", 195 | contentType: "application/json", 196 | success: function(data) { 197 | that.setState({services: data || []}); 198 | } 199 | }); 200 | }, 201 | getInitialState: function() { 202 | this.fetchData(); 203 | return { 204 | services: [] 205 | }; 206 | }, 207 | render: function() { 208 | var that = this; 209 | var results = this.state.services.map(function (result) { 210 | return ( 211 | React.createElement(Result, {key: result.ID, data: result, onSelected: that.selectResult}) 212 | ); 213 | }); 214 | 215 | return ( 216 | React.createElement("div", {className: "recent-panel"}, 217 | React.createElement("div", {className: "container"}, 218 | React.createElement("div", {className: "row"}, 219 | React.createElement("div", {className: "col-xs-12 col-md-3 text-center"}, 220 | React.createElement("h1", null, "Most"), 221 | React.createElement("h1", null, "Recent") 222 | ), 223 | results 224 | ) 225 | ) 226 | ) 227 | ); 228 | } 229 | }); 230 | 231 | var GetStartedPanel = React.createClass({displayName: "GetStartedPanel", 232 | focusOnSearch: function() { 233 | $(".search-panel")[0].scrollIntoView(); 234 | $(".search-box input").focus(); 235 | }, 236 | render: function() { 237 | return ( 238 | React.createElement("div", {className: "get-started-panel"}, 239 | React.createElement("div", {className: "container"}, 240 | React.createElement("div", {className: "row"}, 241 | React.createElement("div", {className: "col-xs-12 text-center"}, 242 | React.createElement("h1", null, 243 | "Why haven't you started yet?" 244 | ) 245 | ) 246 | ), 247 | React.createElement("div", {className: "row"}, 248 | React.createElement("div", {className: "col-xs-12 text-center"}, 249 | React.createElement("h1", null, 250 | "It's ", React.createElement("em", null, "literally ", React.createElement("strong", null, "free")), "." 251 | ) 252 | ) 253 | ), 254 | React.createElement("div", {className: "row"}, 255 | React.createElement("div", {className: "col-xs-12 text-center"}, 256 | React.createElement("h1", null, 257 | "You could even ", React.createElement("strong", null, "make"), " money." 258 | ) 259 | ) 260 | ), 261 | React.createElement("div", {className: "row"}, 262 | React.createElement("div", {className: "col-xs-12 text-center"}, 263 | React.createElement("a", {className: "btn btn-default", href: "javascript:void(0)", onClick: this.focusOnSearch}, 264 | React.createElement("span", {className: "glyphicon glyphicon-search"}), 265 | "Go ahead - search for a service" 266 | ) 267 | ) 268 | ) 269 | ) 270 | ) 271 | ); 272 | } 273 | }); 274 | 275 | var LandingHome = React.createClass({displayName: "LandingHome", 276 | getInitialState: function() { 277 | var waitToPop = /^((?!chrome).)*safari/i.test(navigator.userAgent); 278 | $(window).off("popstate").on("popstate", function() { 279 | if (waitToPop) { 280 | waitToPop = false; 281 | return; 282 | } 283 | window.location = window.location.href; 284 | }); 285 | 286 | return {searchActive: false}; 287 | }, 288 | handleSearchActivated: function() { 289 | this.setState({searchActive: true}); 290 | }, 291 | render: function() { 292 | if (this.state.searchActive) { 293 | return ( 294 | React.createElement("div", {className: "home-page"}, 295 | React.createElement(SearchPanel, null) 296 | ) 297 | ); 298 | } else { 299 | return ( 300 | React.createElement("div", {className: "home-page"}, 301 | React.createElement(SearchPanel, {onSearchActivated: this.handleSearchActivated}), 302 | React.createElement(LonelyPanel, null), 303 | React.createElement(HookPanel, null), 304 | React.createElement(PopularPanel, null), 305 | React.createElement(RecentPanel, null), 306 | React.createElement(GetStartedPanel, null) 307 | ) 308 | ); 309 | } 310 | } 311 | }); 312 | 313 | React.render( 314 | React.createElement(LandingHome, null), 315 | document.getElementById('content') 316 | ); -------------------------------------------------------------------------------- /public/scripts/service.jsx: -------------------------------------------------------------------------------- 1 | var EditButton = React.createClass({ 2 | startEdit: function() { 3 | if ($(".add-code-entry").hasClass("disabled")) { 4 | $(".add-code-entry").removeClass("disabled").select(); 5 | $(".add-code-btn .glyphicon").addClass("fade-out"); 6 | setTimeout(function() { 7 | $(".add-code-btn .glyphicon").removeClass("glyphicon-pencil fade-out").addClass("glyphicon-save fade-in"); 8 | }, 500); 9 | } 10 | }, 11 | finishEdit: function() { 12 | $(".add-code-btn .glyphicon").addClass("fade-out"); 13 | setTimeout(function() { 14 | $(".add-code-btn .glyphicon").removeClass("glyphicon-save fade-out infinite").addClass("glyphicon-pencil fade-in"); 15 | }, 500); 16 | }, 17 | clickButton: function() { 18 | if ($(".add-code-entry").hasClass("disabled")) { 19 | this.startEdit(); 20 | } else { 21 | $(".add-code-entry").addClass("disabled"); 22 | $(".add-code-btn .glyphicon").addClass("spin infinite"); 23 | 24 | if ($(".add-code-entry").val() === this.props.code.Code) { 25 | this.finishEdit(); 26 | return; 27 | } 28 | 29 | var that = this; 30 | $.ajax({ 31 | url: "/codes", 32 | method: "POST", 33 | data: JSON.stringify({ 34 | code: $(".add-code-entry").val(), 35 | serviceId: that.props.serviceId 36 | }), 37 | success: function(code) { 38 | that.props.saved(code) 39 | that.finishEdit(); 40 | }, 41 | error: function(xhr) { 42 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 43 | } 44 | }); 45 | } 46 | }, 47 | render: function() { 48 | return ( 49 | <div className="edit-referral-code"> 50 | <div className="row"> 51 | <div className="col-xs-12"> 52 | <input type="text" className="add-code-entry form-control input-lg disabled" 53 | placeholder="Enter your code..." defaultValue={this.props.code.Code} onClick={this.startEdit} /> 54 | <button className="btn btn-lg btn-default add-code-btn hide-me" onClick={this.clickButton}> 55 | <span className="glyphicon glyphicon-pencil" /> 56 | </button> 57 | </div> 58 | </div> 59 | <div className="row"> 60 | <div className="col-xs-12 referral-code-views"> 61 | <em>{this.props.code.ViewsSinceUpdate} people have viewed your code since {new Date(this.props.code.DateUpdated).toDateString()}</em> 62 | </div> 63 | </div> 64 | </div> 65 | ); 66 | } 67 | }); 68 | 69 | var AddButton = React.createClass({ 70 | showEditBox: function() { 71 | if ($("body").attr("data-logged-in") !== "true") { 72 | $("#authenticate-panel").collapse("show"); 73 | $("#authenticate-panel")[0].scrollIntoView(); 74 | return; 75 | } 76 | 77 | if ($(".add-code-entry").val() !== "") { 78 | $(".add-code-entry").addClass("disabled"); 79 | $(".add-code-entry").prop("disabled", true); 80 | $(".add-code-btn .glyphicon-plus").addClass("spin"); 81 | 82 | var that = this; 83 | $.ajax({ 84 | url: "/codes", 85 | method: "POST", 86 | data: JSON.stringify({ 87 | code: $(".add-code-entry").val(), 88 | serviceId: that.props.serviceId 89 | }), 90 | success: function(code) { 91 | $(".add-code-btn .glyphicon").addClass("fade-out"); 92 | setTimeout(function() { 93 | $(".add-code-btn .glyphicon") 94 | .removeClass("fade-out spin glyphicon-plus") 95 | .addClass("glyphicon-pencil fade-in"); 96 | $(".add-code-entry").prop("disabled", false); 97 | that.props.saved(code); 98 | }, 500); 99 | }, 100 | error: function(xhr) { 101 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 102 | } 103 | }); 104 | } else { 105 | $(".add-code-msg").toggleClass("hide-me"); 106 | $(".add-code-entry").toggleClass("hide-me"); 107 | $(".add-code-btn").toggleClass("hide-me"); 108 | $(".add-code-entry").focus(); 109 | } 110 | }, 111 | render: function() { 112 | return ( 113 | <div className="row add-referral-code"> 114 | <div className="col-xs-12"> 115 | <input type="text" className="add-code-entry hide-me form-control input-lg" placeholder="Enter your code..." /> 116 | <button className="btn btn-lg btn-default add-code-btn" onClick={this.showEditBox}> 117 | <span className="glyphicon glyphicon-plus" /> 118 | <div className="add-code-msg"> 119 | Have your own code? Add it! 120 | </div> 121 | </button> 122 | </div> 123 | </div> 124 | ); 125 | } 126 | }); 127 | 128 | var ReferralCodeEntry = React.createClass({ 129 | onSave: function(code) { 130 | this.props.onUpdate(code); 131 | this.setState({code: code}); 132 | }, 133 | getInitialState: function() { 134 | return { 135 | code: this.props.code 136 | }; 137 | }, 138 | render: function() { 139 | if (this.state.code && this.state.code.ID !== "") { 140 | return ( 141 | <EditButton code={this.state.code} saved={this.onSave} serviceId={this.props.serviceId} /> 142 | ); 143 | } else { 144 | return ( 145 | <AddButton saved={this.onSave} serviceId={this.props.serviceId} /> 146 | ); 147 | } 148 | } 149 | }); 150 | 151 | var ReportButton = React.createClass({ 152 | report: function() { 153 | if ($("body").attr("data-logged-in") !== "true") { 154 | $("#authenticate-panel").collapse("show"); 155 | $("#authenticate-panel")[0].scrollIntoView(); 156 | return; 157 | } 158 | 159 | if ($(".report-code-text").hasClass("hidden")) { 160 | this.props.onReportCode(this.showDefaultButtons); 161 | } else { 162 | this.props.onStartReportCode(); 163 | setTimeout(function() { 164 | $(".report-code-text").addClass("hidden"); 165 | $(".report-code-ask").removeClass("hidden"); 166 | }, 300); 167 | } 168 | }, 169 | cancel: function() { 170 | this.props.onCancelReportCode(); 171 | this.showDefaultButtons(); 172 | }, 173 | showDefaultButtons: function() { 174 | setTimeout(function() { 175 | $(".report-code-text").removeClass("hidden"); 176 | $(".report-code-ask").addClass("hidden"); 177 | }, 300); 178 | }, 179 | render: function() { 180 | return ( 181 | <div className="report-bad-code"> 182 | <span className="report-code-ask hidden">Are you sure this code didn't work?</span> 183 | <button className="btn btn-default btn-xs report-code" onClick={this.report}> 184 | <span className="glyphicon glyphicon-flag"></span> 185 | <span className="report-code-text">Report</span> 186 | <span className="report-code-ask hidden">Yes</span> 187 | </button> 188 | <button className="btn btn-default btn-xs report-code-cancel report-code-ask hidden" onClick={this.cancel}> 189 | <span className="glyphicon glyphicon-ban-circle"></span> 190 | No 191 | </button> 192 | </div> 193 | ); 194 | } 195 | }); 196 | 197 | var ReferralCodeActions = React.createClass({ 198 | getInitialState: function() { 199 | return { 200 | code: this.props.code 201 | }; 202 | }, 203 | componentDidMount: function() { 204 | var zclip = new ZeroClipboard($(".copy-code")); 205 | zclip.on('ready', function(event) { 206 | zclip.on('copy', function(event) { 207 | $(".copy-code .glyphicon").addClass("shake"); 208 | }); 209 | zclip.on('afterCopy', function(event) { 210 | setTimeout(function () { 211 | $(".copy-code .glyphicon").removeClass("shake"); 212 | }, 400); 213 | }); 214 | }); 215 | }, 216 | shuffle: function() { 217 | $(".shuffle-code .glyphicon").addClass("spin fast infinite"); 218 | var that = this; 219 | $.ajax({ 220 | url: "/codes/random?sid=" + that.state.code.ServiceID, 221 | method: "GET", 222 | contentType: "application/json", 223 | success: function(code) { 224 | $(".copy-code").attr("data-clipboard-text", that.state.code.Code); 225 | $(".shuffle-code .glyphicon").removeClass("infinite"); 226 | 227 | that.props.onNewCode(code); 228 | that.setState({code: code}); 229 | }, 230 | error: function(xhr) { 231 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 232 | } 233 | }); 234 | }, 235 | hideButtons: function() { 236 | $(".referral-code-actions").addClass("fade-out"); 237 | setTimeout(function() { 238 | $(".copy-code, .shuffle-code").addClass("hidden"); 239 | $(".referral-code-actions").removeClass("fade-out").addClass("fade-in"); 240 | }, 500); 241 | }, 242 | showButtons: function() { 243 | $(".referral-code-actions").addClass("fade-out"); 244 | setTimeout(function() { 245 | $(".copy-code, .shuffle-code").removeClass("hidden"); 246 | $(".referral-code-actions").removeClass("fade-out").addClass("fade-in"); 247 | }, 500); 248 | }, 249 | report: function(callback) { 250 | $(".report-code .glyphicon").addClass("spin fast infinite"); 251 | 252 | var that = this; 253 | $.ajax({ 254 | url: "/codes/" + that.state.code.ID + "/report", 255 | success: function(code) { 256 | $(".copy-code").attr("data-clipboard-text", that.state.code.Code); 257 | 258 | $(".report-code .glyphicon").removeClass("infinite"); 259 | callback(); 260 | that.showButtons(); 261 | 262 | that.props.onNewCode(code); 263 | that.setState({code: code}); 264 | }, 265 | error: function(xhr) { 266 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 267 | } 268 | }); 269 | }, 270 | render: function() { 271 | return ( 272 | <div className="referral-code-actions"> 273 | <button className="btn btn-default btn-xs copy-code" data-clipboard-text={this.state.code.Code}> 274 | <span className="glyphicon glyphicon-copy"></span> 275 | Clipboard 276 | </button> 277 | <button className="btn btn-default btn-xs shuffle-code" onClick={this.shuffle}> 278 | <span className="glyphicon glyphicon-random"></span> 279 | Shuffle 280 | </button> 281 | <ReportButton onStartReportCode={this.hideButtons} onCancelReportCode={this.showButtons} onReportCode={this.report} /> 282 | </div> 283 | ) 284 | } 285 | }); 286 | 287 | var ReferralCode = React.createClass({ 288 | getInitialState: function() { 289 | return { 290 | code: this.props.code 291 | }; 292 | }, 293 | setCode: function(code) { 294 | this.state.code = code; 295 | $(".referral-code").addClass("fade-out"); 296 | setTimeout(function() { 297 | $(".referral-code").text(code.Code).removeClass("fade-out").addClass("fade-in"); 298 | }, 300); 299 | }, 300 | render: function() { 301 | if (this.state.code && this.state.code.Code !== "") { 302 | return ( 303 | <div className="row random-referral-code"> 304 | <div className="col-xs-12"> 305 | <h3> 306 | Use this referral code: 307 | </h3> 308 | <h1 className="referral-code"> 309 | {this.state.code.Code} 310 | </h1> 311 | <ReferralCodeActions code={this.state.code} onNewCode={this.setCode} /> 312 | </div> 313 | </div> 314 | ); 315 | } else { 316 | if (this.props.userHasCode) { 317 | return ( 318 | <div className="row random-referral-code"> 319 | <h3>Looks like no one has added any codes for this service (except you).</h3> 320 | <h3>Tell your firends, coworkers, or random strangers to add their codes!</h3> 321 | </div> 322 | ) 323 | } else { 324 | return ( 325 | <div className="row random-referral-code"> 326 | <h3>Looks like no one has added any codes for this service.</h3> 327 | <h3>Be the first by clicking the button below.</h3> 328 | </div> 329 | ) 330 | } 331 | } 332 | } 333 | }); 334 | 335 | var ServicePage = React.createClass({ 336 | getInitialState: function() { 337 | return { 338 | code: this.props.data.RandomCode, 339 | name: this.props.data.Name, 340 | url: this.props.data.URL, 341 | description: this.props.data.Description, 342 | id: this.props.data.ID, 343 | userCode: this.props.data.UserCode 344 | }; 345 | }, 346 | onCodeUpdated: function(code) { 347 | if (!this.state.code) { 348 | this.setState({userCode: code}); 349 | } 350 | }, 351 | render: function() { 352 | return ( 353 | <div className="service-area"> 354 | <div className="view-result row"> 355 | <div className="col-xs-12"> 356 | <h1 className="text-center service-name"> 357 | {this.state.name} 358 | </h1> 359 | <h2 className="text-center"> 360 | <a href={"//" + this.state.url} target="blank">{this.state.url}</a> 361 | </h2> 362 | <h4 className="text-center"> 363 | {this.state.description} 364 | </h4> 365 | </div> 366 | </div> 367 | <ReferralCode code={this.state.code} userHasCode={this.state.userCode && this.state.userCode.ID !== ""} /> 368 | <ReferralCodeEntry code={this.state.userCode} serviceId={this.state.id} onUpdate={this.onCodeUpdated} /> 369 | </div> 370 | ); 371 | } 372 | }); -------------------------------------------------------------------------------- /public/scripts/service.js: -------------------------------------------------------------------------------- 1 | var EditButton = React.createClass({displayName: "EditButton", 2 | startEdit: function() { 3 | if ($(".add-code-entry").hasClass("disabled")) { 4 | $(".add-code-entry").removeClass("disabled").select(); 5 | $(".add-code-btn .glyphicon").addClass("fade-out"); 6 | setTimeout(function() { 7 | $(".add-code-btn .glyphicon").removeClass("glyphicon-pencil fade-out").addClass("glyphicon-save fade-in"); 8 | }, 500); 9 | } 10 | }, 11 | finishEdit: function() { 12 | $(".add-code-btn .glyphicon").addClass("fade-out"); 13 | setTimeout(function() { 14 | $(".add-code-btn .glyphicon").removeClass("glyphicon-save fade-out infinite").addClass("glyphicon-pencil fade-in"); 15 | }, 500); 16 | }, 17 | clickButton: function() { 18 | if ($(".add-code-entry").hasClass("disabled")) { 19 | this.startEdit(); 20 | } else { 21 | $(".add-code-entry").addClass("disabled"); 22 | $(".add-code-btn .glyphicon").addClass("spin infinite"); 23 | 24 | if ($(".add-code-entry").val() === this.props.code.Code) { 25 | this.finishEdit(); 26 | return; 27 | } 28 | 29 | var that = this; 30 | $.ajax({ 31 | url: "/codes", 32 | method: "POST", 33 | data: JSON.stringify({ 34 | code: $(".add-code-entry").val(), 35 | serviceId: that.props.serviceId 36 | }), 37 | success: function(code) { 38 | that.props.saved(code) 39 | that.finishEdit(); 40 | }, 41 | error: function(xhr) { 42 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 43 | } 44 | }); 45 | } 46 | }, 47 | render: function() { 48 | return ( 49 | React.createElement("div", {className: "edit-referral-code"}, 50 | React.createElement("div", {className: "row"}, 51 | React.createElement("div", {className: "col-xs-12"}, 52 | React.createElement("input", {type: "text", className: "add-code-entry form-control input-lg disabled", 53 | placeholder: "Enter your code...", defaultValue: this.props.code.Code, onClick: this.startEdit}), 54 | React.createElement("button", {className: "btn btn-lg btn-default add-code-btn hide-me", onClick: this.clickButton}, 55 | React.createElement("span", {className: "glyphicon glyphicon-pencil"}) 56 | ) 57 | ) 58 | ), 59 | React.createElement("div", {className: "row"}, 60 | React.createElement("div", {className: "col-xs-12 referral-code-views"}, 61 | React.createElement("em", null, this.props.code.ViewsSinceUpdate, " people have viewed your code since ", new Date(this.props.code.DateUpdated).toDateString()) 62 | ) 63 | ) 64 | ) 65 | ); 66 | } 67 | }); 68 | 69 | var AddButton = React.createClass({displayName: "AddButton", 70 | showEditBox: function() { 71 | if ($("body").attr("data-logged-in") !== "true") { 72 | $("#authenticate-panel").collapse("show"); 73 | $("#authenticate-panel")[0].scrollIntoView(); 74 | return; 75 | } 76 | 77 | if ($(".add-code-entry").val() !== "") { 78 | $(".add-code-entry").addClass("disabled"); 79 | $(".add-code-entry").prop("disabled", true); 80 | $(".add-code-btn .glyphicon-plus").addClass("spin"); 81 | 82 | var that = this; 83 | $.ajax({ 84 | url: "/codes", 85 | method: "POST", 86 | data: JSON.stringify({ 87 | code: $(".add-code-entry").val(), 88 | serviceId: that.props.serviceId 89 | }), 90 | success: function(code) { 91 | $(".add-code-btn .glyphicon").addClass("fade-out"); 92 | setTimeout(function() { 93 | $(".add-code-btn .glyphicon") 94 | .removeClass("fade-out spin glyphicon-plus") 95 | .addClass("glyphicon-pencil fade-in"); 96 | $(".add-code-entry").prop("disabled", false); 97 | that.props.saved(code); 98 | }, 500); 99 | }, 100 | error: function(xhr) { 101 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 102 | } 103 | }); 104 | } else { 105 | $(".add-code-msg").toggleClass("hide-me"); 106 | $(".add-code-entry").toggleClass("hide-me"); 107 | $(".add-code-btn").toggleClass("hide-me"); 108 | $(".add-code-entry").focus(); 109 | } 110 | }, 111 | render: function() { 112 | return ( 113 | React.createElement("div", {className: "row add-referral-code"}, 114 | React.createElement("div", {className: "col-xs-12"}, 115 | React.createElement("input", {type: "text", className: "add-code-entry hide-me form-control input-lg", placeholder: "Enter your code..."}), 116 | React.createElement("button", {className: "btn btn-lg btn-default add-code-btn", onClick: this.showEditBox}, 117 | React.createElement("span", {className: "glyphicon glyphicon-plus"}), 118 | React.createElement("div", {className: "add-code-msg"}, 119 | "Have your own code? Add it!" 120 | ) 121 | ) 122 | ) 123 | ) 124 | ); 125 | } 126 | }); 127 | 128 | var ReferralCodeEntry = React.createClass({displayName: "ReferralCodeEntry", 129 | onSave: function(code) { 130 | this.props.onUpdate(code); 131 | this.setState({code: code}); 132 | }, 133 | getInitialState: function() { 134 | return { 135 | code: this.props.code 136 | }; 137 | }, 138 | render: function() { 139 | if (this.state.code && this.state.code.ID !== "") { 140 | return ( 141 | React.createElement(EditButton, {code: this.state.code, saved: this.onSave, serviceId: this.props.serviceId}) 142 | ); 143 | } else { 144 | return ( 145 | React.createElement(AddButton, {saved: this.onSave, serviceId: this.props.serviceId}) 146 | ); 147 | } 148 | } 149 | }); 150 | 151 | var ReportButton = React.createClass({displayName: "ReportButton", 152 | report: function() { 153 | if ($("body").attr("data-logged-in") !== "true") { 154 | $("#authenticate-panel").collapse("show"); 155 | $("#authenticate-panel")[0].scrollIntoView(); 156 | return; 157 | } 158 | 159 | if ($(".report-code-text").hasClass("hidden")) { 160 | this.props.onReportCode(this.showDefaultButtons); 161 | } else { 162 | this.props.onStartReportCode(); 163 | setTimeout(function() { 164 | $(".report-code-text").addClass("hidden"); 165 | $(".report-code-ask").removeClass("hidden"); 166 | }, 300); 167 | } 168 | }, 169 | cancel: function() { 170 | this.props.onCancelReportCode(); 171 | this.showDefaultButtons(); 172 | }, 173 | showDefaultButtons: function() { 174 | setTimeout(function() { 175 | $(".report-code-text").removeClass("hidden"); 176 | $(".report-code-ask").addClass("hidden"); 177 | }, 300); 178 | }, 179 | render: function() { 180 | return ( 181 | React.createElement("div", {className: "report-bad-code"}, 182 | React.createElement("span", {className: "report-code-ask hidden"}, "Are you sure this code didn't work?"), 183 | React.createElement("button", {className: "btn btn-default btn-xs report-code", onClick: this.report}, 184 | React.createElement("span", {className: "glyphicon glyphicon-flag"}), 185 | React.createElement("span", {className: "report-code-text"}, "Report"), 186 | React.createElement("span", {className: "report-code-ask hidden"}, "Yes") 187 | ), 188 | React.createElement("button", {className: "btn btn-default btn-xs report-code-cancel report-code-ask hidden", onClick: this.cancel}, 189 | React.createElement("span", {className: "glyphicon glyphicon-ban-circle"}), 190 | "No" 191 | ) 192 | ) 193 | ); 194 | } 195 | }); 196 | 197 | var ReferralCodeActions = React.createClass({displayName: "ReferralCodeActions", 198 | getInitialState: function() { 199 | return { 200 | code: this.props.code 201 | }; 202 | }, 203 | componentDidMount: function() { 204 | var zclip = new ZeroClipboard($(".copy-code")); 205 | zclip.on('ready', function(event) { 206 | zclip.on('copy', function(event) { 207 | $(".copy-code .glyphicon").addClass("shake"); 208 | }); 209 | zclip.on('afterCopy', function(event) { 210 | setTimeout(function () { 211 | $(".copy-code .glyphicon").removeClass("shake"); 212 | }, 400); 213 | }); 214 | }); 215 | }, 216 | shuffle: function() { 217 | $(".shuffle-code .glyphicon").addClass("spin fast infinite"); 218 | var that = this; 219 | $.ajax({ 220 | url: "/codes/random?sid=" + that.state.code.ServiceID, 221 | method: "GET", 222 | contentType: "application/json", 223 | success: function(code) { 224 | $(".copy-code").attr("data-clipboard-text", that.state.code.Code); 225 | $(".shuffle-code .glyphicon").removeClass("infinite"); 226 | 227 | that.props.onNewCode(code); 228 | that.setState({code: code}); 229 | }, 230 | error: function(xhr) { 231 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 232 | } 233 | }); 234 | }, 235 | hideButtons: function() { 236 | $(".referral-code-actions").addClass("fade-out"); 237 | setTimeout(function() { 238 | $(".copy-code, .shuffle-code").addClass("hidden"); 239 | $(".referral-code-actions").removeClass("fade-out").addClass("fade-in"); 240 | }, 500); 241 | }, 242 | showButtons: function() { 243 | $(".referral-code-actions").addClass("fade-out"); 244 | setTimeout(function() { 245 | $(".copy-code, .shuffle-code").removeClass("hidden"); 246 | $(".referral-code-actions").removeClass("fade-out").addClass("fade-in"); 247 | }, 500); 248 | }, 249 | report: function(callback) { 250 | $(".report-code .glyphicon").addClass("spin fast infinite"); 251 | 252 | var that = this; 253 | $.ajax({ 254 | url: "/codes/" + that.state.code.ID + "/report", 255 | success: function(code) { 256 | $(".copy-code").attr("data-clipboard-text", that.state.code.Code); 257 | 258 | $(".report-code .glyphicon").removeClass("infinite"); 259 | callback(); 260 | that.showButtons(); 261 | 262 | that.props.onNewCode(code); 263 | that.setState({code: code}); 264 | }, 265 | error: function(xhr) { 266 | noty({text: xhr.statusText, layout: 'topLeft', timeout: 7500, type: 'error', theme: 'refermadness'}); 267 | } 268 | }); 269 | }, 270 | render: function() { 271 | return ( 272 | React.createElement("div", {className: "referral-code-actions"}, 273 | React.createElement("button", {className: "btn btn-default btn-xs copy-code", "data-clipboard-text": this.state.code.Code}, 274 | React.createElement("span", {className: "glyphicon glyphicon-copy"}), 275 | "Clipboard" 276 | ), 277 | React.createElement("button", {className: "btn btn-default btn-xs shuffle-code", onClick: this.shuffle}, 278 | React.createElement("span", {className: "glyphicon glyphicon-random"}), 279 | "Shuffle" 280 | ), 281 | React.createElement(ReportButton, {onStartReportCode: this.hideButtons, onCancelReportCode: this.showButtons, onReportCode: this.report}) 282 | ) 283 | ) 284 | } 285 | }); 286 | 287 | var ReferralCode = React.createClass({displayName: "ReferralCode", 288 | getInitialState: function() { 289 | return { 290 | code: this.props.code 291 | }; 292 | }, 293 | setCode: function(code) { 294 | this.state.code = code; 295 | $(".referral-code").addClass("fade-out"); 296 | setTimeout(function() { 297 | $(".referral-code").text(code.Code).removeClass("fade-out").addClass("fade-in"); 298 | }, 300); 299 | }, 300 | render: function() { 301 | if (this.state.code && this.state.code.Code !== "") { 302 | return ( 303 | React.createElement("div", {className: "row random-referral-code"}, 304 | React.createElement("div", {className: "col-xs-12"}, 305 | React.createElement("h3", null, 306 | "Use this referral code:" 307 | ), 308 | React.createElement("h1", {className: "referral-code"}, 309 | this.state.code.Code 310 | ), 311 | React.createElement(ReferralCodeActions, {code: this.state.code, onNewCode: this.setCode}) 312 | ) 313 | ) 314 | ); 315 | } else { 316 | if (this.props.userHasCode) { 317 | return ( 318 | React.createElement("div", {className: "row random-referral-code"}, 319 | React.createElement("h3", null, "Looks like no one has added any codes for this service (except you)."), 320 | React.createElement("h3", null, "Tell your firends, coworkers, or random strangers to add their codes!") 321 | ) 322 | ) 323 | } else { 324 | return ( 325 | React.createElement("div", {className: "row random-referral-code"}, 326 | React.createElement("h3", null, "Looks like no one has added any codes for this service."), 327 | React.createElement("h3", null, "Be the first by clicking the button below.") 328 | ) 329 | ) 330 | } 331 | } 332 | } 333 | }); 334 | 335 | var ServicePage = React.createClass({displayName: "ServicePage", 336 | getInitialState: function() { 337 | return { 338 | code: this.props.data.RandomCode, 339 | name: this.props.data.Name, 340 | url: this.props.data.URL, 341 | description: this.props.data.Description, 342 | id: this.props.data.ID, 343 | userCode: this.props.data.UserCode 344 | }; 345 | }, 346 | onCodeUpdated: function(code) { 347 | if (!this.state.code) { 348 | this.setState({userCode: code}); 349 | } 350 | }, 351 | render: function() { 352 | return ( 353 | React.createElement("div", {className: "service-area"}, 354 | React.createElement("div", {className: "view-result row"}, 355 | React.createElement("div", {className: "col-xs-12"}, 356 | React.createElement("h1", {className: "text-center service-name"}, 357 | this.state.name 358 | ), 359 | React.createElement("h2", {className: "text-center"}, 360 | React.createElement("a", {href: "//" + this.state.url, target: "blank"}, this.state.url) 361 | ), 362 | React.createElement("h4", {className: "text-center"}, 363 | this.state.description 364 | ) 365 | ) 366 | ), 367 | React.createElement(ReferralCode, {code: this.state.code, userHasCode: this.state.userCode && this.state.userCode.ID !== ""}), 368 | React.createElement(ReferralCodeEntry, {code: this.state.userCode, serviceId: this.state.id, onUpdate: this.onCodeUpdated}) 369 | ) 370 | ); 371 | } 372 | }); --------------------------------------------------------------------------------