/*
TODO
------
~ Animations & Transitions
~ fontAwesome
*/
@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400);
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Source Sans Pro", sans-serif;
}
$colorArray: #1289FE,
#075DB2,
#0B5298,
#0B5298,
#0B5298,
#073765;
$red: #ea0000;
$blue: #1289fe;
$gray: #e5e5ea;
$hero-shadow: 1px 1px 0px $gray,
2px 2px 0px $gray,
3px 3px 0px $gray;
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(50%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fade {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-50%);
}
}
.fadeIn {
animation: fadeIn 0.25s;
animation-fill-mode: backwards;
animation-delay: 0.25s;
}
.fadeOut {
animation: fadeOut 0.25s;
animation-fill-mode: backwards;
}
#vue {
margin: 0 auto;
padding: 1em;
text-align: center;
width: 100%;
max-width: 600px;
.hero {
margin: 0.5em auto 0.25em;
font-size: 3em;
color: $blue;
text-shadow: $hero-shadow;
}
.login {
margin: 0;
}
.notification {
margin-top: 0;
margin-bottom: 1.5em;
&.bad {
color: $red;
}
}
input {
display: block;
appearance: none;
margin: 0.5em auto;
padding: 0.5em;
vertical-align: middle;
border: 1px solid $gray;
border-radius: 2px;
font-size: 1em;
width: 100%;
max-width: 20em;
&.messageInput {
display: inline-block;
width: calc(100% - 3.5em);
}
}
button {
appearance: none;
padding: 0.5em;
vertical-align: middle;
border: none;
background: none;
color: $blue;
font-weight: 400;
font-size: 1em;
cursor: pointer;
transition: color 0.25s ease-out;
&:hover {
color: darken($blue, 15%);
}
}
.messagesWrapper {
padding: 0.5em;
max-height: 500px;
overflow-y: scroll;
.message {
margin: 2em auto;
padding: 0 1em;
padding-right: 2em;
background: $gray;
border-radius: 1em;
position: relative;
text-align: left;
//width: 100%;
span, p {
display: inline-block;
vertical-align: middle;
}
span {
position: absolute;
top: -1.25em;
left: 0.75em;
right: auto;
font-size: 0.85em;
color: darken($gray, 15%);
white-space: nowrap;
vertical-align: top;
time {
display: none;
vertical-align: top;
}
}
&:hover {
span {
time {
display: inline;
}
}
}
p {
width: auto;
}
i {
display: none;
font-size: 1em;
vertical-align: middle;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0.5em;
}
&[data-person="Me"] {
background: $blue;
color: white;
&:after {
content: "";
position: absolute;
right: -0.5em;
bottom: 0;
width: 0.5em;
height: 1em;
border-left: 0.5em solid $blue;
border-bottom-left-radius: 1em 0.5em;
}
span {
right: 0.75em;
left: auto;
color: $blue;
}
i {
display: inline-block;
cursor: pointer;
}
}
&:not([data-person="Me"]) {
&:after {
content: "";
position: absolute;
left: -0.5em;
bottom: 0;
width: 0.5em;
height: 1em;
border-right: 0.5em solid $gray;
border-bottom-right-radius: 1em 0.5em;
}
}
i {
padding: 0.25em;
cursor: pointer;
}
}
}
.usersTyping {
text-align: left;
font-size: 0.85em;
margin: 0 auto;
color: darken($gray, 15%);
}
.usersOnline {
text-decoration: underline;
margin-bottom: 0.25em;
}
.users {
margin: 0 auto;
}
.addMessage {
margin: 1em auto;
text-align: left;
}
.chatroomFlexContainer {
display: flex;
flex-flow: row wrap;
justify-content: space-around;
margin: 1em auto;
.chatroom {
position: relative;
flex-grow: 1;
margin: 0.5em;
padding: 1em;
color: white;
border-radius: 5px;
box-shadow: 0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);
transition: all 0.1s;
cursor: pointer;
i.fa-close {
opacity: 0;
position: absolute;
top: 0.25em;
right: 0.25em;
padding: 0.5em;
cursor: pointer;
transition: opacity 0.1s;
}
i.fa-cog {
opacity: 0;
position: absolute;
top: 0.25em;
left: 0.25em;
padding: 0.5em;
cursor: pointer;
transition: opacity 0.1s;
}
&:hover {
box-shadow: 0 12px 15px 0 rgba(0,0,0,0.24), 0 15px 30px 0 rgba(0,0,0,0.19);
i {
opacity: 1;
}
}
$i: 1;
@while $i < length($colorArray)+1 {
&:nth-of-type(#{$i}n) {
$color: nth($colorArray, $i);
background-color: $color;
$i: $i + 1;
}
}
}
}
.modalWrapper {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
.overlay {
position: fixed;
z-index: 0;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
}
.modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
z-index: 1;
padding: 1em;
border-radius: 5px;
h2 {
margin-bottom: 0;
}
p.notification {
margin-top: 0;
color: $red;
}
i.fa-close {
position: absolute;
top: 0.5em;
right: 0.5em;
cursor: pointer;
}
}
}
}
View Compiled
/*
TODO
------
~ Image uploads? (base64 encode)
~ localStorage to remember youre logged in
~ use LS from Index
*/
//---------
// Global Data
//---------
var baseURL = "https://messenger-app-16b49.firebaseio.com",
chatroomsRef = new Firebase(baseURL + "/chatrooms"),
chatroomRef = chatroomsRef.push(),
chatroomKey = "",
messagesRef = "",
messageRef = "",
presenceRef = new Firebase(baseURL + "/.info/connected"),
usersRef = new Firebase(baseURL + "/users"),
userRef = usersRef.push(),
myRef = "",
myKey = "",
newMessage = {},
newUser = {},
timer = null,
loginAttempts = 0,
notifications = {
"landing": "Try to be original, your username might be taken.",
"error": "We have encountered an error, please reload your page and try again.",
"invalid": "Sorry, your password is invalid. Make sure you have everything correct. Caps matter!",
"loggedIn": "This user is already logged in.",
"lockedOut": "You have attempted to login too many times, please come back later and try again.",
"duplicateChat": "Sorry, there is already a chatroom with this name. Try again!"
},
loginButtonText = {
"new": "Create Account",
"existing": "Log In"
};
//---------
// Components
//---------
var presence = Vue.extend({
template: '<h3 class="usersOnline">Users Online: {{ users | usersOnline }}</h3>' +
'<p class="users" v-for="user in users">' +
'{{ user | isMyUsernameOrOnline }}' +
'</p>'
,
data: function() {
return {
newUser: newUser,
newMessage: newMessage
}
},
firebase: {
users: usersRef
}
});
var login = Vue.extend({
template: '<div transition="fade">' +
'<h2 class="login">Login, or create an account below.</h2>' +
'<p class="notification {{ notificationStatus }}">{{ notification }}</p>' +
'<input class="nameInput" @keyup="isExistingUser()" @keyup.enter="goToChatrooms()" v-model="newUser.username" placeholder="Username"/>' +
'<input class="nameInput" type="password" @keyup.tab="isExistingUser()" @keyup.enter="goToChatrooms()" v-model="newUser.password" placeholder="Password"/>' +
'<button @click="goToChatrooms()">{{ loginButtonText }}</button>' +
'<presence></presence>' +
'</div>'
,
data: function() {
return {
newMessage: newMessage,
newUser: newUser,
loginAttempts: loginAttempts,
notification: notifications.landing,
notificationStatus: "good",
loginButtonText: loginButtonText.new
}
},
beforeCompile: function() {
if(myRef != "" && typeof(myRef) != "undefined") {
myRef.update({typing: false, online: false});
myRef = "";
myKey = "";
//is there a better way?
newMessage = {
text: "",
username: "",
timestamp: ""
};
this.newMessage = newMessage;
newUser = {
username: "",
password: ""
};
this.newUser = newUser;
}
},
methods: {
isExistingUser: function() {
for(var i = 0; i < this.users.length; i++) {
if(this.newUser.username == this.users[i].username) {
this.loginButtonText = loginButtonText.existing;
return;
}
}
this.loginButtonText = loginButtonText.new;
},
goToChatrooms: function() {
if(this.loginAttempts < 5) {
for(var i = 0; i < this.users.length; i++) {
if(this.newUser.username == this.users[i].username) {
if(this.newUser.password != this.users[i].password) {
this.loginAttempts++;
this.notificationStatus = "bad";
this.notification = notifications.invalid;
return;
}
if(!!this.users[i].online) {
this.loginAttempts++;
this.notificationStatus = "bad";
this.notification = notifications["loggedIn"];
return;
}
myKey = this.users[i][".key"];
myRef = new Firebase(baseURL + "/users/" + myKey);
}
}
if(this.newUser.username.trim() != "" && this.newUser.password.trim() != "") {
this.newMessage.username = this.newUser.username;
router.go("/chatrooms");
}
} else {
this.notificationStatus = "bad";
this.notification = notifications["lockedOut"];
return;
}
}
},
firebase: {
users: usersRef
}
});
var chatrooms = Vue.extend({
template: '<div transition="fade">' +
'<h2 class="createChatroom">Search the current chatrooms, or create a new one.</h2>' +
'<input @keyup.esc="resetFilter()" v-model="searchString" type="text" placeholder="Search chatrooms"/>' +
'<p>- or -</p>' +
'<button @click="toggleNewChatroomModal()">Create Chatroom</button>' +
'<div class="chatroomFlexContainer">' +
'<div class="chatroom" v-for="chatroom in chatrooms | filterBy searchString" @click="goToMessages(chatroom)">' +
'<i v-if="chatroom.creator == myUsername" @click.stop="removeChatroom(chatroom[\'.key\'])" class="fa fa-close"></i>' +
'<i v-if="chatroom.creator == myUsername" @click.stop="toggleEditChatroomModal(chatroom)" class="fa fa-cog"></i>' +
'<h3>{{ chatroom.name }}</h3>' +
'<p>{{ chatroom.messages | messageLength }} messages</p>' +
'</div>' +
'</div>' +
'<presence></presence>' +
'</div>' +
'<div v-show="showNewModal" class="modalWrapper" @keyup.esc="toggleNewChatroomModal()">' +
'<div class="overlay" @click="toggleNewChatroomModal()"></div>' +
'<div class="modal">' +
'<i class="fa fa-close" @click="toggleNewChatroomModal()"></i>' +
'<h2 class="modalHeader">Name your new chatroom</h2>' +
'<p>{{ notification }}</p>' +
'<input class="modalName" @keyup.enter="createChatroom()" v-model="newChatroomName" placeholder="Chatroom name"/>' +
'<button @click="createChatroom()">Create Chatroom</button>' +
'</div>' +
'</div>' +
'<div v-show="showEditModal" class="modalWrapper" @keyup.esc="toggleEditChatroomModal()">' +
'<div class="overlay" @click="toggleEditChatroomModal()"></div>' +
'<div class="modal">' +
'<i class="fa fa-close" @click="toggleEditChatroomModal()"></i>' +
'<h2 class="modalHeader">Edit Chatroom</h2>' +
'<p class="notification">{{ notification }}</p>' +
'<input class="modalName" @keyup.enter="updateChatroom()" v-model="editChatroomName" placeholder="Chatroom name"/>' +
'<button @click="updateChatroom()">Update Chatroom Name</button>' +
'<p>- or -</p>' +
'<button @click="resetChatroom()"><i class="fa fa-rotate-left"></i> Reset Chat Messages</button>' +
'</div>' +
'</div>' +
'</div>'
,
beforeCompile: function() {
if(newUser.username == "" || newUser.password == "") {
router.go("/");
return;
}
},
ready: function() {
var self = this;
presenceRef.on("value", function(snap) {
if (snap.val()) {
if(myKey == "" && newUser.username != "" && newUser.password != "") {
userRef.set({
online: true,
username: newUser.username,
password: newUser.password,
typing: false
}, function() {
myKey = self.users[self.users.length - 1][".key"];
myRef = new Firebase(baseURL + "/users/" + myKey);
myRef.onDisconnect().update({typing: false, online: false});
});
} else if(typeof(myKey) != "undefined" && myKey != "") {
myRef.update({online: true});
myRef.onDisconnect().update({typing: false, online: false});
}
}
});
},
methods: {
toggleNewChatroomModal: function() {
this.showNewModal = !this.showNewModal;
this.notification = "";
this.newChatroomName = "";
},
toggleEditChatroomModal: function(chatroom) {
this.showEditModal = !this.showEditModal;
this.notification = "";
if(!!chatroom) {
this.editChatroomName = chatroom.name;
this.editChatroomKey = chatroom[".key"];
} else {
this.editChatroomName = "";
this.editChatroomKey = "";
}
},
updateChatroom: function() {
for(var i = 0; i < this.chatrooms.length; i++) {
if(this.editChatroomName == this.chatrooms[i].name) {
this.notification = notifications.duplicateChat;
return;
}
}
chatroomsRef.child(this.editChatroomKey).update({name: this.editChatroomName});
this.showEditModal = false;
},
resetChatroom: function() {
chatroomsRef.child(this.editChatroomKey).update({messages: {}});
this.showEditModal = false;
},
removeChatroom: function(key) {
chatroomsRef.child(key).remove();
},
createChatroom: function() {
var self = this;
for(var i = 0; i < this.chatrooms.length; i++) {
if(this.newChatroomName == this.chatrooms[i].name) {
this.notification = notifications.duplicateChat;
return;
}
}
chatroomRef.set({
name: self.newChatroomName,
//population: 0,
messages: {},
creator: self.myUsername
}, function() {
self.toggleNewChatroomModal();
self.newChatroomName = "";
chatroomRef = chatroomsRef.push();
});
},
goToMessages: function(chatroom) {
chatroomKey = chatroom['.key'];
router.go("/messenger");
},
resetFilter: function() {
this.searchString = "";
}
},
filters: {
messageLength: function(messages) {
if(messages) {
return Object.keys(messages).length;
} else {
return 0;
}
}
},
firebase: {
chatrooms: chatroomsRef,
users: usersRef
},
data: function() {
return {
showNewModal: false,
showEditModal: false,
newChatroomName: "",
editChatroomName: "",
editChatroomKey: "",
notification: "",
searchString: "",
myUsername: newUser.username
}
}
});
var messenger = Vue.extend({
template: '<div transition="fade">' +
'<div class="messagesWrapper">' +
'<div class="message" v-for="message in messages" data-person="{{ message.username | isMyMessage }}">' +
'<span>{{ message.username | isMyMessage }} <time class="hidden">@ {{ message.timestamp }}</time></span>' +
'<p>{{ message.text }}</p>' +
'<i data-person="{{ message.username | isMyMessage }}" @click="removeMessage($key)" class="fa fa-close"></i>' +
'</div>' +
'<p class="usersTyping" v-for="user in users">' +
'{{ user | isTyping }}' +
'</p>' +
'</div>' +
'<input class="messageInput" @keyup.enter="addMessage()" v-model="newMessage.text" @keyup="userTyping($event)"/>' +
'<button @click="addMessage()">Send</button>' +
'<presence></presence>' +
'</div>'
,
beforeCompile: function() {
var self = this;
if(newUser.username == "" || newUser.password == "") {
router.go("/");
return;
}
messagesRef = new Firebase(baseURL + "/chatrooms/" + chatroomKey + "/messages");
messageRef = messagesRef.push();
messagesRef.once("value", function(snapshot) {
var data = snapshot.val();
self.messages = data;
});
},
ready: function() {
var self = this;
presenceRef.on("value", function(snap) {
if (snap.val()) {
myRef.update({online: true});
myRef.onDisconnect().update({typing: false, online: false});
}
});
messagesRef.on("value", function(snapshot) {
var data = snapshot.val();
self.messages = data;
self.scrollToBottomOfMessages();
});
},
data: function() {
return {
newMessage: newMessage,
newUser: newUser,
messages: {}
}
},
firebase: {
users: usersRef
},
methods: {
removeMessage: function(key) {
messagesRef.child(key).remove();
},
addMessage: function() {
if (this.newMessage.text.trim() != "") {
usersRef.child(myKey).update({typing: false});
messagesRef.push().set({
text: this.newMessage.text,
username: this.newMessage.username,
timestamp: this.timestamp()
});
this.newMessage.text = "";
this.newMessage.timestamp = "";
}
},
scrollToBottomOfMessages: function() {
Vue.nextTick(function() {
var messages = document.getElementsByClassName("messagesWrapper")[0];
if(!!messages) {
messages.scrollTop = messages.scrollHeight;
}
})
},
userTyping: function(e) {
//only numbers, letters, spaces, and backspace
if (e.keyCode >= 48 && e.keyCode <= 57 || e.keyCode >= 65 && e.keyCode <= 90 || e.keyCode == 33 || e.keyCode == 4) {
usersRef.child(myKey).update({typing: true});
clearTimeout(timer);
timer = setTimeout(function() {
usersRef.child(myKey).update({typing: false});
}, 1000);
}
},
timestamp: function() {
var date = new Date();
function formatDate(date) {
var monthNames = [
"January", "February", "March",
"April", "May", "June", "July",
"August", "September", "October",
"November", "December"
];
var day = date.getDate(),
monthIndex = date.getMonth(),
year = date.getFullYear();
return monthNames[monthIndex] + " " + day + ", " + year
}
function formatTime(date) {
var hour = date.getHours(),
minute = date.getMinutes(),
amPM = (hour > 11) ? "pm" : "am";
if(hour > 12) {
hour -= 12;
} else if(hour == 0) {
hour = "12";
}
if(minute < 10) {
minute = "0" + minute;
}
return hour + ":" + minute + amPM;
}
return formatTime(date) + " " + formatDate(date)
},
convertImgToBase64: function(url, callback, outputFormat){
var img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function() {
var canvas = document.createElement('CANVAS');
var ctx = canvas.getContext('2d');
canvas.height = this.height;
canvas.width = this.width;
ctx.drawImage(this,0,0);
var dataURL = canvas.toDataURL(outputFormat || 'image/png');
callback(dataURL);
canvas = null;
};
img.src = url;
},
encodeLocalImageFileAsURL: function(cb) {
return function(){
var file = this.files[0];
var reader = new FileReader();
reader.onloadend = function() {
cb(reader.result);
}
reader.readAsDataURL(file);
}
}
},
filters: {
isMyMessage: function(username) {
if(username === this.newMessage.username) {
return "Me"
} else {
return username
}
},
isTyping: function(user) {
var self = this;
if(user.typing && user.username != self.newMessage.username) {
self.scrollToBottomOfMessages();
return user.username + " is typing..."
}
}
}
});
//---------
// Global Filters
//---------
Vue.filter("isMyUsernameOrOnline", function(user) {
if(user.username === this.newMessage.username) {
return user.username + " (Me)"
} else {
if(user.online == true) {
return user.username
}
}
});
Vue.filter("usersOnline", function(users) {
var numberOnline = 0;
for(var i = 0; i < users.length; i++) {
if(users[i].online == true) {
numberOnline++;
}
}
return numberOnline;
});
//---------
// Transitions
//---------
Vue.transition('fade', {
enterClass: 'fadeIn',
leaveClass: 'fadeOut'
})
//---------
// Register global components
//---------
Vue.component('presence', presence)
//---------
// Router
//---------
var router = new VueRouter();
router.map({
'/': {
component: login
},
'/chatrooms': {
component: chatrooms
},
'/messenger': {
component: messenger
}
})
router.start(Vue, '#vue');