├── 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 |
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 |
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 |
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 |
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 |
Name
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 | });
25 |
26 | var CreateServiceURL = React.createClass({
27 | render: function() {
28 | return (
29 |
39 | );
40 | }
41 | });
42 |
43 | var CreateServiceDescription = React.createClass({
44 | render: function() {
45 | return (
46 |
47 |
Description
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 |
118 |
119 | Create Service
120 |
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 |
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 |
37 |
38 | Load More
39 |
40 |
41 |
42 | );
43 | }
44 | });
45 |
46 | var Title = React.createClass({
47 | render: function() {
48 | return (
49 |
54 | )
55 | }
56 | });
57 |
58 | var SmallTitle = React.createClass({
59 | render: function() {
60 | return (
61 |
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 |
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 |
98 |
100 |
101 | Sign Up or Log In
102 |
103 |
104 | )
105 | }
106 | });
107 |
108 | var AccountButton = React.createClass({
109 | render: function() {
110 | return (
111 |
112 |
113 | Account
114 |
115 | );
116 | }
117 | });
118 |
119 | var LogoutButton = React.createClass({
120 | render: function() {
121 | return (
122 |
123 |
124 | Log out
125 |
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 |
140 |
141 |
Let's get you authenticated.
142 |
143 |
Why should I?
144 |
Authentication helps prevent malicious users from submitting bad or duplicate referral codes and prevents robots from taking over the site.
145 |
Why Google?
146 |
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 |
Where's the legal information?
148 |
You can view the privacy policy and terms of service on
the legal page .
149 |
150 |
151 |
152 | Sign in with Google
153 |
154 |
By signing in using the link above, you agree to the Terms and Conditions .
155 |
156 |
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 |
181 | )
182 | } else {
183 | return (
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
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": "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",
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Add
24 |
25 |
26 | New
27 |
28 |
29 |
30 |
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 |
69 | );
70 | });
71 |
72 | return (
73 |
74 | {results}
75 |
76 |
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 |
98 |
100 |
101 | );
102 | } else {
103 | return (
104 |
105 |
107 |
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 |
227 |
228 |
229 | );
230 | } else if (this.state.selected === -1) {
231 | return (
232 |
233 |
234 |
235 | 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 |
242 |
244 |
245 |
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 |
13 |
14 |
15 |
Looking for referral links?
16 | Start searching below to find your product or service.
17 |
18 |
19 |
20 | );
21 | }
22 | });
23 |
24 | var LonelyPanel = React.createClass({
25 | render: function() {
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
No friends?
34 |
35 |
36 |
41 |
42 |
43 |
44 |
45 |
No family?
46 |
47 |
48 |
53 |
54 |
55 |
56 |
57 |
No followers?
58 |
59 |
60 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | );
82 | }
83 | });
84 |
85 | var HookPanel = React.createClass({
86 | render: function() {
87 | return (
88 |
89 |
90 |
91 |
92 |
Find a random referral code to get mutual discounts
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
Then submit your own for others to use
103 |
104 |
105 |
106 |
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 |
152 | );
153 | });
154 |
155 | return (
156 |
157 |
158 |
159 |
160 |
Most
161 | Popular
162 |
163 | {results}
164 |
165 |
166 |
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 |
212 | );
213 | });
214 |
215 | return (
216 |
217 |
218 |
219 |
220 |
Most
221 | Recent
222 |
223 | {results}
224 |
225 |
226 |
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 |
239 |
240 |
241 |
242 |
243 | Why haven't you started yet?
244 |
245 |
246 |
247 |
248 |
249 |
250 | It's literally free .
251 |
252 |
253 |
254 |
255 |
256 |
257 | You could even make money.
258 |
259 |
260 |
261 |
269 |
270 |
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 |
295 |
296 |
297 | );
298 | } else {
299 | return (
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 | );
309 | }
310 | }
311 | });
312 |
313 | React.render(
314 | ,
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 |
48 | );
49 | });
50 | return (
51 |
52 |
Your Services
53 |
54 | {services}
55 |
56 |
this.state.services.length} onMore={this.fetchServices} />
57 |
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 |
97 |
98 |
99 |
100 | Use Different Google Identity
101 |
102 |
103 |
104 | );
105 | } else {
106 | return (
107 |
108 |
109 | Change which Google identity you use to authenticate?
110 |
111 |
112 | Yup, take me to Google
113 |
114 |
115 |
116 | Nevermind
117 |
118 |
119 |
120 | );
121 | }
122 | }
123 | });
124 |
125 | var CancelAccountDeletion = React.createClass({
126 | render: function() {
127 | return (
128 |
129 |
130 | Cancel
131 |
132 | );
133 | }
134 | });
135 |
136 | var VerifyAccountDeletionDesparation = React.createClass({
137 | render: function() {
138 | return (
139 |
140 |
141 | Wait! Don't go! I never got the chance to tell you, but... I love you!
142 |
143 |
144 |
145 | Sorry, pal, but the feeling's not mutual
146 |
147 |
148 |
149 | );
150 | }
151 | });
152 |
153 | var VerifyAccountDeletionApology = React.createClass({
154 | render: function() {
155 | return (
156 |
157 |
158 | ...Er. Sorry about that. Overreaction on my part! Please don't tell my supervisor.
159 |
160 |
161 |
162 | Sure, I can be discreet, let's get on with this
163 |
164 |
165 |
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 |
184 |
185 | Continuing will permanantly delete your account and remove your codes from the system.
186 |
187 |
188 | If you really want to leave, please enter your Google username in the textbox below .
189 |
190 |
200 |
201 |
202 | Permanently Delete Account
203 |
204 |
205 |
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 |
233 |
234 |
235 |
236 | Delete Refer Madness Account
237 |
238 |
239 |
240 |
241 |
242 |
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 |
256 |
257 | You are currently logged in as {this.state.username}
258 |
259 |
260 |
261 |
262 | );
263 | }
264 | });
265 |
266 | var AccountPage = React.createClass({
267 | render: function() {
268 | return (
269 |
270 |
271 |
272 |
273 |
274 | );
275 | }
276 | });
277 |
278 | React.render(
279 | ,
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 |
50 |
51 |
52 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {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({
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 |
114 |
115 |
116 |
117 |
118 |
119 | Have your own code? Add it!
120 |
121 |
122 |
123 |
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 |
142 | );
143 | } else {
144 | return (
145 |
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 |
182 | Are you sure this code didn't work?
183 |
184 |
185 | Report
186 | Yes
187 |
188 |
189 |
190 | No
191 |
192 |
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 |
273 |
274 |
275 | Clipboard
276 |
277 |
278 |
279 | Shuffle
280 |
281 |
282 |
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 |
304 |
305 |
306 | Use this referral code:
307 |
308 |
309 | {this.state.code.Code}
310 |
311 |
312 |
313 |
314 | );
315 | } else {
316 | if (this.props.userHasCode) {
317 | return (
318 |
319 |
Looks like no one has added any codes for this service (except you).
320 | Tell your firends, coworkers, or random strangers to add their codes!
321 |
322 | )
323 | } else {
324 | return (
325 |
326 |
Looks like no one has added any codes for this service.
327 | Be the first by clicking the button below.
328 |
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 |
354 |
355 |
356 |
357 | {this.state.name}
358 |
359 |
362 |
363 | {this.state.description}
364 |
365 |
366 |
367 |
368 |
369 |
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 | });
--------------------------------------------------------------------------------