Skip to content
Browse files

imported code from private repository

  • Loading branch information...
1 parent 7c3ccbd commit ee7ec0aee048b7f62e9a30ec030048801545ed87 @hbbio hbbio committed Sep 2, 2013
View
31 Makefile
@@ -0,0 +1,31 @@
+########################################
+# USER VARIABLES
+EXE = birdy.exe
+ifdef SystemRoot
+ RUN_CMD = $(EXE)
+else
+ RUN_CMD = ./$(EXE)
+endif
+
+PACKNAME =
+SRC =
+PCKDIR = ./plugins/
+PCK =
+PLUGIN =
+PLUGINDIR =
+OTHER_DEPENDS = resources/*
+CONF_FILE = opa.conf
+OPAOPT = --parser js-like
+
+#Compiler variables
+FLAG = --opx-dir _build --import-package stdlib.database.mongo
+PORT = 8080
+
+RUN_OPT =
+
+default: exe
+
+run: exe
+ $(RUN_CMD) $(RUN_OPT) || true
+
+include Makefile.common
View
89 Makefile.common
@@ -0,0 +1,89 @@
+########################################
+# MAKEFILE VARIABLES
+OPACOMPILER ?= opa --slicer-dump
+OPA = $(OPACOMPILER) --parser classic $(FLAG) $(OPAOPT)
+PWD ?= $(shell pwd)
+BUILDDIR ?= $(PWD)/_build
+export BUILDDIR
+BUILDDOCDIR ?=$(PWD)/doc/
+export BUILDDOCDIR
+PACKDOC = $(BUILDDOCDIR)/$(PACKNAME:%.opx=%.doc)/
+BUILDDOC = $(PACKDOC)
+OPAGENAPI ?= $(OPACOMPILER) --api-only
+OPADOC ?= opadoc-gen.exe
+DEPENDS = $(SRC) $(PCK:%=$(BUILDDIR)/%) $(PLUGIN:%=$(BUILDDIR)/%)
+
+ifdef CONF_FILE
+ CONFIG = --conf $(CONF_FILE) --conf-opa-files
+else
+ CONFIG =
+endif
+
+_ = $(shell mkdir -p $(BUILDDIR))
+
+########################################
+# MAIN RULE
+exe : $(EXE)
+pack : $(PACKNAME)
+doc : $(PACKDOC) doc.sub
+
+########################################
+# MAIN PACKAGE BUILDING
+$(PACKNAME) : $(BUILDDIR)/$(PACKNAME)
+
+$(BUILDDIR)/$(PACKNAME) : $(DEPENDS)
+ @echo "### Building package $(PACKNAME)"
+ mkdir -p $(BUILDDIR)
+ $(OPA) --autocompile $(SRC) $(PLUGIN:%=$(BUILDDIR)/%) --build-dir $(BUILDDIR) -I $(BUILDDIR) $(OPAOPT)
+ @rm -rf $(BUILDDIR)/$(PACKNAME)
+ @mv $(PACKNAME) $(BUILDDIR)/
+
+########################################
+# SUBS PACKAGE/PLUGIN BUILDING
+$(BUILDDIR)/%.opx :
+ make $(@:$(BUILDDIR)/%.opx=-C $(PCKDIR)/%) pack
+
+$(BUILDDIR)/%.opp :
+ make $(@:$(BUILDDIR)/%.opp=-C $(PLUGINDIR)/%)
+
+########################################
+# EXECUTABLE BUILDING
+$(EXE) : pack $(SRC) $(MAINSRC) $(DEPENDS) $(OTHER_DEPENDS)
+ @echo "### Building executable $(EXE) "
+ mkdir -p $(BUILDDIR)
+ $(OPA) $(COMPILOPT) $(MAINSRC) $(CONFIG) $(PLUGIN:%=$(BUILDDIR)/%) -o $@ -I $(BUILDDIR) --build-dir $(BUILDDIR)/$(EXE)
+
+$(EXE:%.exe=%.run) : $(EXE)
+ ./$(EXE) -p $(PORT)
+
+########################################
+# DOCUMENTATION BUILDING - Dirty...
+$(PACKDOC) :
+ @echo "### Building documentation $(PACKNAME:%.opx=%.doc)"
+ @mkdir -p $(BUILDDOC)
+ @$(OPACOMPILER) $(SRC) $(PLUGIN:%=$(BUILDDIR)/%) --api-only -I $(BUILDDIR)
+ @mv $(SRC:%=%.api) $(SRC:%=%.api-txt) $(BUILDDOC)
+ @cp $(SRC) $(BUILDDOC)
+ @cd $(BUILDDOC) && $(OPADOC) .
+ @mkdir -p $(PACKDOC)
+ @mv doc/*.html doc/*.css $(PACKDOC)
+
+doc.sub :
+ @if [ -n "$(PCK)" ]; then make $(PCK:%.opx=-C $(PCKDIR)/%) doc; fi
+
+########################################
+# Used by recursives makefile
+pack.depends :
+ @echo $(PCK) $(PLUGIN)
+
+########################################
+# CLEANING
+clean ::
+ @$(PCK:%.opx=make -C $(PCKDIR)/% clean &&) $(PLUGIN:%.opp=make -C $(PLUGINDIR)/% clean &&) echo "### Cleaning $(BUILDDIR)";
+ @rm -rf $(BUILDDIR)/* _tracks/*;
+ @if [ -n "$(EXE)" ]; then rm -rf $(EXE); fi
+ @if [ -n "$(PACKDOC)" ]; then rm -rf $(PACKDOC); fi
+
+deep-clean :: clean
+ @rm -rf opa-debug
+
View
23 opa.conf
@@ -0,0 +1,23 @@
+birdy.controller:
+ import birdy.{model,view}
+ src/controller/main.opa
+
+birdy.view:
+ import birdy.model
+ import stdlib.widgets.bootstrap.{dropdown,modal,alert}
+ import stdlib.themes.bootstrap
+ import stdlib.web.client
+ import stdlib.web.forms
+ src/view/page.opa
+ src/view/signup.opa
+ src/view/signin.opa
+ src/view/topbar.opa
+ src/view/msg.opa
+
+birdy.model:
+ import stdlib.web.mail
+ import stdlib.web.mail.smtp.client
+ src/model/data.opa
+ src/model/user.opa
+ src/model/msg.opa
+ src/model/topic.opa
View
1 package.json
@@ -0,0 +1 @@
+{"name":"link","version":"1.1.1","main":"birdy.exe","dependencies":{"opabsl.opp":"1.1.1","opa-js-runtime-cps":"1.1.1","birdy.controller.opx":"1.1.1"}}
View
148 resources/css/style.css
@@ -0,0 +1,148 @@
+@import url(http://fonts.googleapis.com/css?family=Sanchez);
+/* Variables */
+html {
+ background: url('/resources/img/bg.png') repeat 0 0;
+}
+body,
+.hero-unit {
+ background: transparent;
+}
+body {
+ padding-top: 40px;
+ font-family: 'Sanchez', sans-serif;
+}
+textarea {
+ width: 100%;
+ min-height: 60px;
+}
+a {
+ color: #1188ff;
+}
+a:hover {
+ color: #006edd;
+}
+.btn-primary {
+ background: #1188ff;
+}
+.btn-primary:hover {
+ background: #006edd;
+}
+.control-group.error > label,
+.control-group.error .help-block,
+.control-group.error .help-inline,
+.control-group.error .checkbox,
+.control-group.error .radio {
+ color: #ff0077;
+}
+.control-group.error input,
+.control-group.error select,
+.control-group.error textarea {
+ color: #ff0077;
+ border-color: #ff0077;
+}
+.alert-danger,
+.alert-error {
+ color: #ff0077;
+ background-color: #ffe5f1;
+ border-color: #ffcce4;
+}
+/* Landing page */
+.hero-unit {
+ min-height: 250px;
+ text-align: center;
+}
+.hero-unit h1 {
+ font-size: 80px;
+ line-height: 120px;
+}
+.hero-unit h2 {
+ margin-bottom: 30px;
+}
+/* Forms */
+.help-inline {
+ font-size: 12px;
+ color: gray;
+ line-height: 16px;
+}
+.form-horizontal .help-inline {
+ display: block;
+ padding-left: 0;
+}
+.form-horizontal input {
+ display: block;
+ width: 325px;
+}
+.btn-large {
+ width: 130px;
+}
+.form-wrap {
+ width: 530px;
+ margin: 0 auto;
+ position: relative;
+}
+/* Notifications */
+#notice {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 38px;
+}
+#notice .alert {
+ text-align: center;
+}
+#signin_result .alert {
+ padding-left: 35px;
+ text-align: center;
+}
+/* Messages */
+.well {
+ border-color: #aeaeae;
+ padding-bottom: 10px;
+}
+.author-info span {
+ color: gray;
+ font-size: 0.9em;
+}
+.user-info h3,
+.user-info #follow_btn {
+ display: inline-block;
+}
+.user-info h3 {
+ margin: 15px 10px 15px 0;
+ line-height: 28px;
+}
+.user-info #follow_btn {
+ margin-top: 15px;
+ vertical-align: top;
+}
+/* Modal */
+.modal-backdrop,
+.modal-backdrop.fade.in {
+ opacity: .2;
+ filter: alpha(opacity=20);
+}
+.modal-body textarea {
+ width: 516px;
+}
+.modal-body #preview_container {
+ background-color: lightgray;
+ margin: 5px -15px -15px;
+ padding: 15px;
+}
+.modal-footer {
+ text-align: center;
+}
+.post-footer {
+ text-align: right;
+}
+.char-wrap {
+ color: gray;
+ margin: 10px 5px 0 0;
+ vertical-align: middle;
+}
+.char-wrap .char-error {
+ color: red;
+}
+.char-wrap .char-warning {
+ color: orange;
+}
View
91 resources/css/style.less
@@ -0,0 +1,91 @@
+@import url(http://fonts.googleapis.com/css?family=Sanchez);
+
+/* Variables */
+@linkcolor:#18F;
+@errorcolor:#F07;
+
+html {background: url('/resources/img/bg.png') repeat 0 0;}
+body, .hero-unit {background:transparent;}
+body {
+ padding-top: 40px;
+ font-family: 'Sanchez', sans-serif;
+}
+textarea {width: 100%;min-height: 60px;}
+
+a {
+ color:@linkcolor;
+ &:hover {color:darken(@linkcolor,10%);}
+}
+.btn-primary {
+ background: @linkcolor;
+ &:hover {background:darken(@linkcolor,10%);}
+}
+.control-group.error {
+ > label, .help-block, .help-inline, .checkbox, .radio {
+ color: @errorcolor;
+ }
+ input, select, textarea {
+ color: @errorcolor;
+ border-color: @errorcolor;
+ }
+}
+.alert-danger, .alert-error {
+ color: @errorcolor;
+ background-color: lighten(@errorcolor,45%);
+ border-color: lighten(@errorcolor,40%);
+}
+
+/* Landing page */
+.hero-unit {
+ min-height: 250px;
+ text-align: center;
+ h1 {font-size:80px; line-height: 120px;}
+ h2 {margin-bottom: 30px;}
+}
+
+/* Forms */
+.help-inline {font-size: 12px;color: gray;line-height: 16px;}
+.form-horizontal {
+ .help-inline {display: block;padding-left:0;}
+ input {display: block;width: 325px;}
+}
+.btn-large {width:130px;}
+.form-wrap {width:530px;margin:0 auto;position: relative;}
+
+/* Notifications */
+#notice {
+ position:absolute;
+ left:0;
+ right:0;
+ top:38px;
+
+ .alert {text-align: center;}
+ }
+#signin_result .alert {padding-left:35px;text-align: center;}
+
+/* Messages */
+.well {border-color:lighten(gray,18%);padding-bottom: 10px;}
+.author-info span {color:gray;font-size: 0.9em;}
+
+.user-info {
+ h3, #follow_btn {display: inline-block;}
+ h3 {margin: 15px 10px 15px 0;line-height: 28px;}
+ #follow_btn {margin-top: 15px;vertical-align: top;}
+}
+
+/* Modal */
+.modal-backdrop, .modal-backdrop.fade.in {opacity: .2;filter: alpha(opacity=20);}
+.modal-body {
+ textarea {width: 516px;}
+ #preview_container {background-color: lightgray;margin: 5px -15px -15px;padding: 15px;}
+
+}
+.modal-footer {text-align: center;}
+.post-footer {text-align: right;}
+.char-wrap {
+ color:gray;margin:10px 5px 0 0;vertical-align: middle;
+ .char {
+ &-error {color:red;}
+ &-warning {color:orange;}
+ }
+}
View
BIN resources/img/bg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN resources/img/bg_@2X.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
32 src/controller/main.opa
@@ -0,0 +1,32 @@
+module Controller {
+
+ // URL dispatcher of your application; add URL handling as needed
+ function dispatcher(Uri.relative url) {
+ match (url) {
+ case {path: ["activation", activation_code] ...}:
+ Signup.activate_user(activation_code)
+ case {path:["user", user | _] ...}:
+ Page.user_page(user)
+ case {path:["topic", topic | _] ...}:
+ Page.topic_page(topic)
+ default:
+ match (User.get_logged_user()) {
+ case {~user}: Page.user_page(User.get_name(user))
+ default: Page.main_page()
+ }
+ }
+ }
+
+}
+
+resources = @static_resource_directory("resources")
+
+Server.start(Server.http, [
+ { register:
+ [ { doctype: { html5 } },
+ { css: ["/resources/css/style.css"] }
+ ]
+ },
+ { ~resources },
+ { dispatch: Controller.dispatcher }
+])
View
11 src/model/data.opa
@@ -0,0 +1,11 @@
+database birdy {
+ User.info /users[{username}]
+ Msg.t /msgs[{author, created_at}]
+}
+
+module Data {
+
+ main_host = "localhost"
+ main_port = ":8080"
+
+}
View
5 src/model/db.opa
@@ -0,0 +1,5 @@
+database birdy {
+ User.info /users[{username}]
+ Msg.t /msgs[{author, created_at}]
+}
+
View
96 src/model/msg.opa
@@ -0,0 +1,96 @@
+abstract type Msg.t =
+ { string content,
+ User.t author,
+ Date.date created_at,
+ list(Topic.t) topic_refs,
+ list(User.name) user_refs
+ }
+
+type Msg.segment =
+ { string text } or
+ { Uri.uri link } or
+ { User.name user } or
+ { Topic.t topic }
+
+module Msg {
+
+ private function list(Topic.t) get_all_topics(list(Msg.segment) msg) {
+ function filter_topics(seg) {
+ match (seg) {
+ case ~{topic}: some(topic)
+ default: none
+ }
+ }
+ List.filter_map(filter_topics, msg)
+ }
+
+ private function list(User.name) get_all_users(list(Msg.segment) msg) {
+ function filter_users(seg) {
+ match (seg) {
+ case ~{user}: some(user)
+ default: none
+ }
+ }
+ List.filter_map(filter_users, msg)
+ }
+
+ function Msg.t create(User.t author, string content) {
+ msg_segs = analyze_content(content)
+ { ~content, ~author,
+ created_at: Date.now(),
+ topic_refs: get_all_topics(msg_segs),
+ user_refs: get_all_users(msg_segs)
+ }
+ }
+
+ function get_author(Msg.t msg) { msg.author }
+ function get_created_at(Msg.t msg) { msg.created_at }
+
+ private function list(Msg.segment) analyze_content(string msg) {
+ word = parser { case word=([a-zA-Z0-9_\-]+) -> Text.to_string(word) }
+ element = parser {
+ case "@" user=word: ~{user}
+ case "#" topic=word: ~{topic}
+ /* careful here, Uri.uri_parser too liberal, as it parses things like
+ hey.ho as valid URLs; so we use "http://" prefix to recognize URLs */
+ case &"http://" url=Uri.uri_parser: {link: url}
+ }
+ segment_parser = parser {
+ case ~element: element
+ /* below we eat a complete [word] or a single non-word character; the
+ latter case alone may not be enough as we don't want:
+ sthhttp://sth to pass for an URL. */
+ case text=word: {~text}
+ case c=(.): {text: Text.to_string(c)}
+ }
+ msg_parser = parser { case res=segment_parser*: res }
+ Parser.parse(msg_parser, msg)
+ }
+
+ function list(Msg.segment) analyze(Msg.t msg) {
+ analyze_content(msg.content)
+ }
+
+ function int length(Msg.t msg) {
+ String.length(msg.content)
+ }
+
+ exposed function void store(Msg.t msg) {
+ /birdy/msgs[{author:msg.author, created_at:msg.created_at}] <- msg;
+ }
+
+ function msgs_for_user(User.t user) {
+ userdata = /birdy/users[{username: user.username}]
+ /birdy/msgs[author.username in userdata.follows_users or
+ topic_refs[_] in userdata.follows_topics or
+ user_refs[_] == user.username or
+ author.username == user.username;
+ order -created_at;
+ limit 50]
+ }
+
+ function msgs_for_topic(Topic.t topic) {
+ /birdy/msgs[topic_refs[_] == topic; order -created_at; limit 50]
+ }
+
+}
View
9 src/model/topic.opa
@@ -0,0 +1,9 @@
+abstract type Topic.t = string
+
+module Topic {
+
+ function Topic.t create(string topic) {
+ topic
+ }
+
+}
View
195 src/model/user.opa
@@ -0,0 +1,195 @@
+abstract type User.name = string
+
+abstract type User.status = {active} or {string activation_code}
+
+abstract type User.info =
+ { Email.email email,
+ string username,
+ string passwd,
+ User.status status,
+ list(User.name) follows_users,
+ list(Topic.t) follows_topics
+ }
+
+type User.t = { Email.email email, User.name username }
+
+type User.logged = {guest} or {User.t user}
+
+module User {
+
+ @xmlizer(User.t) function user_to_xml(user) {
+ <>{user.username}</>
+ }
+
+ @stringifier(User.t) function user_to_string(user) {
+ user.username
+ }
+
+ private UserContext.t(User.logged) logged_user = UserContext.make({guest})
+
+ function string get_name(User.t user) {
+ user.username
+ }
+
+ private function send_registration_email(args) {
+ from = Email.of_string("no-reply@{Data.main_host}")
+ subject = "Birdy says welcome"
+ email =
+ <p>Hello {args.username}!</p>
+ <p>Thank you for registering with Birdy.</p>
+ <p>Activate your account by clicking on
+ <a href="http://{Data.main_host}{Data.main_port}/activation/{args.activation_code}">this link</a>.
+ </p>
+ <p>Happy messaging!</p>
+ <p>--------------</p>
+ <p>The Birdy Team</p>
+ content = {html: email}
+ continuation = function(_) { void }
+ SmtpClient.try_send_async(from, args.email, subject, content, Email.default_options, continuation)
+ }
+
+ private function User.t mk_view(User.info info) {
+ {username: info.username, email: info.email}
+ }
+
+ function option(User.t) with_username(string name) {
+ ?/birdy/users[{username: name}] |> Option.map(mk_view, _)
+ }
+
+ exposed function outcome register(user) {
+ activation_code = Random.string(15)
+ status =
+ #<Ifstatic:NO_ACTIVATION_MAIL>
+ {active}
+ #<Else>
+ {~activation_code}
+ #<End>
+ user =
+ { email: user.email,
+ username: user.username,
+ passwd: user.passwd,
+ follows_users: [],
+ follows_topics: [],
+ ~status
+ }
+ x = ?/birdy/users[{username: user.username}]
+ match (x) {
+ case {none}:
+ /birdy/users[{username: user.username}] <- user
+ #<Ifstatic:NO_ACTIVATION_MAIL>
+ void
+ #<Else>
+ send_registration_email({~activation_code, username:user.username, email: user.email})
+ #<End>
+ {success}
+ case {some: _}:
+ {failure: "User with the given name already exists."}
+ }
+ }
+
+ exposed function outcome activate_account(activation_code) {
+ user = /birdy/users[status == ~{activation_code}]
+ |> DbSet.iterator
+ |> Iter.to_list
+ |> List.head_opt
+ match (user) {
+ case {none}: {failure}
+ case {some: user}:
+ /birdy/users[{username: user.username}] <- {user with status: {active}}
+ {success}
+ }
+ }
+
+ exposed function outcome(User.t, string) login(username, passwd) {
+ x = ?/birdy/users[~{username}]
+ match (x) {
+ case {none}: {failure: "This user does not exist."}
+ case {some: user}:
+ match (user.status) {
+ case {activation_code: _}:
+ {failure: "You need to activate your account by clicking the link we sent you by email."}
+ case {active}:
+ if (user.passwd == passwd) {
+ user_view = mk_view(user)
+ UserContext.set(logged_user, {user: user_view})
+ {success: user_view}
+ } else
+ {failure: "Incorrect password. Try again."}
+ }
+ }
+ }
+
+ function User.logged get_logged_user() {
+ UserContext.get(logged_user)
+ }
+
+ private function do_if_logged_in(action) {
+ match (get_logged_user()) {
+ case {guest}: void
+ case {user: me}: action(me)
+ }
+ }
+
+ function follow_user(user) {
+ function mk_follow(me) {
+ /birdy/users[{username: me.username}]/follows_users <+ user.username
+ }
+ do_if_logged_in(mk_follow)
+ }
+
+ function unfollow_user(user) {
+ function mk_unfollow(me) {
+ /birdy/users[username == me.username]/follows_users <-- [user.username]
+ }
+ do_if_logged_in(mk_unfollow)
+ }
+
+ function isFollowing_user(user) {
+ match (get_logged_user()) {
+ case {guest}: {unapplicable}
+ case {user: me}:
+ if (user.username == me.username) {
+ {unapplicable}
+ } else {
+ if (/birdy/users[username == me.username and follows_users[_] == user.username]
+ |> DbSet.iterator |> Iter.is_empty) {
+ {not_following}
+ } else {
+ {following}
+ }
+ }
+ }
+ }
+
+ function follow_topic(topic) {
+ function mk_follow(me) {
+ /birdy/users[{username: me.username}]/follows_topics <+ topic
+ }
+ do_if_logged_in(mk_follow)
+ }
+
+ function unfollow_topic(topic) {
+ function mk_unfollow(me) {
+ /birdy/users[username == me.username]/follows_topics <-- [topic]
+ }
+ do_if_logged_in(mk_unfollow)
+ }
+
+ function isFollowing_topic(topic) {
+ match (get_logged_user()) {
+ case {guest}: {unapplicable}
+ case {user: me}:
+ if (/birdy/users[username == me.username and follows_topics[_] == topic]
+ |> DbSet.iterator |> Iter.is_empty) {
+ {not_following}
+ } else {
+ {following}
+ }
+ }
+ }
+
+ function logout() {
+ UserContext.set(logged_user, {guest})
+ }
+
+}
View
111 src/view/msg.opa
@@ -0,0 +1,111 @@
+module MsgUI {
+
+ window_id = "msgbox"
+
+ private preview_content_id = "preview_content"
+ private input_box_id = "input_box"
+ private chars_left_id = "chars_left"
+ private submit_btn_id = "submit_btn"
+
+ private MAX_MSG_LENGTH = 140
+ private MSG_WARN_LENGTH = 120
+
+ private function render_segment(Msg.segment seg) {
+ match (seg) {
+ case ~{user}:
+ <b><a class=ref-user href="/user/{user}">@{user}</a></b>
+ case ~{topic}:
+ <i><a class=ref-topic href="/topic/{topic}">#{topic}</a></i>
+ case ~{link}:
+ <a href={link}>{Uri.to_string(link)}</a>
+ case ~{text}:
+ <>{text}</>
+ }
+ }
+
+ function xhtml render(Msg.t msg) {
+ msg_author = Msg.get_author(msg)
+ <div class=well>
+ <p class="author-info">
+ <strong><a href="/user/{msg_author}">@{msg_author}</a></strong>
+ <span>{Date.to_string(Msg.get_created_at(msg))}</span>
+ </p>
+ <p>
+ {List.map(render_segment, Msg.analyze(msg))}
+ </p>
+ </div>
+ }
+
+ private client function get_msg(user) {
+ Dom.get_value(#{input_box_id})
+ |> Msg.create(user, _)
+ }
+
+ private client function close() {
+ Modal.hide(#{window_id})
+ }
+
+ private client function update_preview(user)(_) {
+ msg = get_msg(user)
+ #{preview_content_id} = render(msg)
+
+ // show status
+ msg_len = Msg.length(msg)
+ #{chars_left_id} = MAX_MSG_LENGTH - msg_len
+ remove = Dom.remove_class
+ add = Dom.add_class
+ remove(#{chars_left_id}, "char-error");
+ remove(#{chars_left_id}, "char-warning");
+ remove(#{submit_btn_id}, "disabled");
+ Dom.set_enabled(#{submit_btn_id}, true);
+
+ if (msg_len > MAX_MSG_LENGTH) {
+ add(#{chars_left_id}, "char-error");
+ add(#{submit_btn_id}, "disabled");
+ Dom.set_enabled(#{submit_btn_id}, false);
+ } else if (msg_len > MSG_WARN_LENGTH) {
+ add(#{chars_left_id}, "char-warning");
+ }
+ }
+
+ private function submit(user)(_) {
+ get_msg(user) |> Msg.store;
+ Dom.clear_value(#{input_box_id});
+ close();
+ Client.reload();
+ }
+
+ function modal_window_html() {
+ match (User.get_logged_user()) {
+ case {guest}: <></>
+ case ~{user}:
+ win_body =
+ <textarea id={input_box_id} onready={update_preview(user)} onkeyup={update_preview(user)} placeholder="Compose a message"/>
+ <div id=#preview_container>
+ <p class=badge>Preview</p>
+ <div id={preview_content_id} />
+ </div>
+ win_footer =
+ <span class="char-wrap pull-left">
+ <span id={chars_left_id} class="char"/>
+ characters left
+ </span>
+ <button id={submit_btn_id} disabled=disabled class="pull-right btn btn-large btn-primary disabled" onclick={submit(user)}>
+ Post
+ </button>
+ Modal.make(window_id, <>What's on your mind?</>, win_body, win_footer, Modal.default_options)
+ }
+ }
+
+ function html() {
+ match (User.get_logged_user()) {
+ case {guest}: <></>
+ case {user: _}:
+ <a class="btn btn-primary pull-right" data-toggle=modal href="#{window_id}">
+ <i class="icon-edit icon-white" />
+ New message
+ </a>
+ }
+ }
+
+}
View
106 src/view/page.opa
@@ -0,0 +1,106 @@
+module Page {
+
+ function alert(message, cl) {
+ <div class="alert alert-{cl}">
+ <button type="button" class="close" data-dismiss="alert">×</button>
+ {message}
+ </div>
+ }
+
+ function page_template(title, content, notice) {
+ html =
+ <div class="navbar navbar-fixed-top">
+ <div class=navbar-inner>
+ <div class=container>
+ {Topbar.html()}
+ </div>
+ </div>
+ </div>
+ <div id=#main>
+ <span id=#notice class=container>{notice}</span>
+ {content}
+ {MsgUI.modal_window_html()}
+ {Signin.modal_window_html()}
+ {Signup.modal_window_html()}
+ </div>
+ Resource.page(title, html)
+ }
+
+ main_page_content =
+ <div class=hero-unit>
+ <h1>Birdy</h1>
+ <h2>Micro-blogging platform.<br/>
+ Built with <a href="http://opalang.org">Opa.</a>
+ </h2>
+ <p>{Signup.signup_btn_html}</p>
+ </div>
+
+ function main_page() {
+ page_template("Birdy", main_page_content, <></>)
+ }
+
+ private function msgs_page(msgs, title, header, follow, unfollow, isFollowing) {
+ recursive function do_follow(_) {
+ _ = follow();
+ #follow_btn = follow_btn();
+ }
+ and function do_unfollow(_) {
+ _ = unfollow();
+ #follow_btn = follow_btn();
+ }
+ and function follow_btn() {
+ match (isFollowing()) {
+ case {unapplicable}: <></>
+ case {following}: <a class="btn" onclick={do_unfollow}>Unfollow</a>
+ case {not_following}: <a class="btn btn-primary" onclick={do_follow}><i class="icon icon-white icon-plus"/> Follow</a>
+ }
+ }
+ msgs_iter = DbSet.iterator(msgs)
+ msgs_html = Iter.map(MsgUI.render, msgs_iter)
+ content =
+ <div class=container>
+ <div class=user-info>
+ {header}
+ <div id=#follow_btn>{follow_btn()}</div>
+ </div>
+ {if (isFollowing() == {unapplicable} && Iter.is_empty(msgs_iter)) {
+ <div class="well">
+ <p>You don't have any messages yet. <a data-toggle=modal href="#{MsgUI.window_id}">Compose a new message</a>.</p>
+ </div>
+ } else <></>}
+ <div id=#msgs>
+ {msgs_html}
+ </div>
+ </div>
+ page_template(title, content, <></>)
+ }
+
+ function topic_page(topic_name) {
+ topic = Topic.create(topic_name)
+ msgs = Msg.msgs_for_topic(topic)
+ title = "#{topic}"
+ header = <h3>{title}</h3>
+ function follow() { User.follow_topic(topic) }
+ function unfollow() { User.unfollow_topic(topic) }
+ function isFollowing() { User.isFollowing_topic(topic) }
+ msgs_page(msgs, title, header, follow, unfollow, isFollowing)
+ }
+
+ function user_page(username) {
+ match (User.with_username(username)) {
+ case {some: user}:
+ msgs = Msg.msgs_for_user(user)
+ title = "@{username}"
+ header = <h3>{title}</h3>
+ function follow() { User.follow_user(user) }
+ function unfollow() { User.unfollow_user(user) }
+ function isFollowing() { User.isFollowing_user(user) }
+ msgs_page(msgs, title, header, follow, unfollow, isFollowing)
+ case {none}:
+ page_template("Unknown user: {username}", <></>,
+ alert("User {username} does not exist", "error")
+ )
+ }
+ }
+
+}
View
76 src/view/signin.opa
@@ -0,0 +1,76 @@
+module Signin {
+
+ window_id = "signin"
+
+ private fld_username =
+ Field.text_field({Field.new with
+ label: "Username",
+ required: {with_msg: <>Please enter your username.</>}
+ })
+
+ private fld_passwd =
+ Field.passwd_field({Field.new with
+ label: "Password",
+ required: {with_msg: <>Please enter your password.</>}
+ })
+
+ private function register(_) {
+ Modal.hide(#{window_id});
+ Modal.show(#{Signup.window_id});
+ }
+
+ private function signin(redirect, _) {
+ username = Field.get_value(fld_username) ? error("Cannot get login")
+ passwd = Field.get_value(fld_passwd) ? error("Cannot get passwd")
+ match (User.login(username, passwd)) {
+ case {failure: msg}:
+ #signin_result =
+ <div class="alert alert-error">
+ {msg}
+ </div>
+ // FIXME This certainly should not be that complex... what am I missing?
+ Dom.transition(#signin_result, Dom.Effect.sequence([
+ Dom.Effect.with_duration({immediate}, Dom.Effect.hide()),
+ Dom.Effect.with_duration({slow}, Dom.Effect.fade_in())
+ ])) |> ignore
+ case {success: _}:
+ match (redirect) {
+ case {none}: Client.reload()
+ case {some:url}: Client.goto(url)
+ }
+ }
+ }
+
+ function form() {
+ form = Form.make(signin(some("/"), _), {})
+ fld = Field.render(form, _)
+ form_body =
+ <div class="signin_form">
+ <legend>Sign in and start messaging</legend>
+ {fld(fld_username)}
+ {fld(fld_passwd)}
+ <a href="#" class="btn btn-primary btn-large"
+ onclick={Form.submit_action(form)}>Sign in</>
+ </div>
+ Form.render(form, form_body)
+ }
+
+ function modal_window_html() {
+ form = Form.make(signin(none, _), {})
+ fld = Field.render(form, _)
+ form_body =
+ <>
+ {fld(fld_username)}
+ {fld(fld_passwd)}
+ <div id=#signin_result />
+ <div class="control-group">
+ <div class="controls">New to Birdy? <a onclick={register}>Sign up</>.</div>
+ </div>
+ </>
+ win_body = Form.render(form, form_body)
+ win_footer =
+ <a href="#" class="btn btn-primary btn-large" onclick={Form.submit_action(form)}>Sign in</>
+ Modal.make(window_id, <>Sign in</>, win_body, win_footer, Modal.default_options)
+ }
+
+}
View
87 src/view/signup.opa
@@ -0,0 +1,87 @@
+module Signup {
+
+ window_id = "signup"
+
+ private fld_username =
+ Field.text_field({Field.new with
+ label: "Username",
+ required: {with_msg: <>Please enter your username.</>},
+ hint: <>Username is publicly visible. You will use it to sign in.</>
+ })
+
+ private fld_passwd =
+ Field.passwd_field({Field.new with
+ label: "Password",
+ required: {with_msg: <>Please enter your password.</>},
+ hint: <>Password should be at least 6 characters long and contain at least one digit.</>,
+ validator: {passwd: Field.default_passwd_validator}
+ })
+
+ private fld_passwd2 =
+ Field.passwd_field({Field.new with
+ label: "Repeat password",
+ required: {with_msg: <>Please repeat your password.</>},
+ validator: {equals: fld_passwd, err_msg: <>Your passwords do not match.</>}
+ })
+
+ private fld_email =
+ Field.email_field({Field.new with
+ label: "Email",
+ required: {with_msg: <>Please enter a valid email address.</>},
+ hint: <>Your activation link will be sent to this address.</>
+ })
+
+ private client function signup(_) {
+ email = Field.get_value(fld_email) ? error("Cannot read form email")
+ username = Field.get_value(fld_username) ? error("Cannot read form name")
+ passwd = Field.get_value(fld_passwd) ? error("Cannot read form passwd")
+ Modal.hide(#{window_id})
+ new_user = ~{email, username, passwd}
+ #notice =
+ match (User.register(new_user)) {
+ case {success: _}:
+ Page.alert("Congratulations! You are successfully registered. You will receive an email with account activation instructions shortly.", "success")
+ case {failure: msg}:
+ Page.alert("Your registration failed: {msg}", "error")
+ }
+ }
+
+ signup_btn_html =
+ <a class="btn btn-large btn-primary" data-toggle=modal href="#{window_id}">
+ Sign up
+ </a>
+
+ function modal_window_html() {
+ form = Form.make(signup, {})
+ fld = Field.render(form, _)
+ form_body =
+ <>
+ {fld(fld_username)}
+ {fld(fld_email)}
+ {fld(fld_passwd)}
+ {fld(fld_passwd2)}
+ </>
+ win_body = Form.render(form, form_body)
+ win_footer =
+ <a href="#" class="btn btn-primary btn-large" onclick={Form.submit_action(form)}>Sign up</>
+ Modal.make(window_id, <>New to Birdy?</>, win_body, win_footer, Modal.default_options)
+ }
+
+ function activate_user(activation_code) {
+ notice =
+ match (User.activate_account(activation_code)) {
+ case {success: _}:
+ Page.alert("Your account is activated now.", "success") <+>
+ <div class="hero-unit">
+ <div class="well form-wrap">
+ {Signin.form()}
+ </div>
+ </div>
+ case {failure: _}:
+ Page.alert("Activation code is invalid.", "error") <+>
+ Page.main_page_content
+ }
+ Page.page_template("Account activation", <></>, notice)
+ }
+
+}
View
45 src/view/topbar.opa
@@ -0,0 +1,45 @@
+module Topbar {
+
+ signinup_btn_html =
+ <ul class="nav pull-right">
+ <li>
+ <a data-toggle=modal href="#{Signin.window_id}">Sign in</a>
+ </li>
+ </ul>
+
+ private function logout(_) {
+ User.logout();
+ Client.reload()
+ }
+
+ private function user_box(username) {
+ id = Dom.fresh_id()
+ <ul id={id} class="nav pull-right">
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ {username}
+ <b class="caret"></b>
+ </a>
+ <ul class=dropdown-menu>
+ <li><a onclick={logout} href="#">Sign out</></>
+ </>
+ </>
+ </>
+ }
+
+ function user_menu() {
+ match (User.get_logged_user()) {
+ case {guest}: signinup_btn_html
+ case ~{user}: user_box(user.username)
+ }
+ }
+
+ function html() {
+ <a class=brand href="/">
+ Birdy
+ </a> <+>
+ MsgUI.html() <+>
+ user_menu()
+ }
+
+}

0 comments on commit ee7ec0a

Please sign in to comment.
Something went wrong with that request. Please try again.