<commit_before>var _ = require('underscore');

//
// Confguration
//
//var CODE_SERVER = "http://130.211.117.91:3001";
var CODE_SERVER = "http://localhost:3001";

// Colors to highlight controlled clients.
var HIGHLIGHT_COLORS = ['#3cba54', '#f4c20d', '#db3236', '#4885ed'];

//
// Helper methods
//
function DefaultClientCode(text) {
  return (
`canvas.writeText(screen.width/2, screen.height/2-300, "Chicago Brick Live!", "#f4c20d", "140px Arial", {textAlign: "center"});
canvas.writeText(screen.width/2, screen.height/2, "${text}", "white", "100px Arial", {textAlign: "center"});
canvas.draw.image(10, 10, "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", 0.5);`
  );
}

function ClientCodeError(message) {
  return (
`canvas.writeText(screen.width/2, screen.height/2-300, "Chicago Brick Live - Code Error", "#d50f25", "120px Arial", {textAlign: "center"});
canvas.writeText(screen.width/2, screen.height/2, "${message}", "white", "80px Arial", {textAlign: "center"});
canvas.draw.image(10, 10, "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", 0.5);`
  );
}

function getClientKey(client) {
  return `${client.x},${client.y}`;
}
//
// Server Module
//
class LiveClientServer extends ServerModuleInterface {
  constructor(config) {
    super();
    this.log = require('debug')('module:live_client');

    var inst = this;
    this.codeServer = require('socket.io-client')(CODE_SERVER);

    // All clients (x, y) that have ever connected.  Used to notify all clients
    // of global code changes (e.g., codeserver comes online) and cache code.
    this.clients = {};

    // Setup connection to code server.
    inst.codeServer.on('connect', function () {
      inst.log(`Connected to code server (${CODE_SERVER}).`);

      // When code server connection is made re-request code for all clients
      // we know about.
      // #TODO Is there any client list provided by brick?
      for (var key in inst.clients) {
        inst.requestCode(inst.clients[key].client);
      }
    });

    inst.codeServer.on('disconnect', function () {
      inst.log('Disconnected from code server.');
    });

    // TODO: These listeners are never removed, fix this.
    inst.codeServer.on('code', function(data) {
      // Make a unique key of the form 'x,y' so we can use a dictionary for clients.
      var key = getClientKey(data.client);
      inst.log(`Received new info for client(${key}).`);

      // Cache the code in case the code server goes away.
      inst.clients[key] = data;

      // Forward code to clients.
      network.emit(`code(${key})`, data);
    });

    // Handle connections from clients.
    network.on('connection', function(socket) {
      socket.on('requestCode', function(data) {

        var key = getClientKey(data.client);
        inst.log(`Client(${key}) requested code.`);
        inst.log(`Code server connected: ${inst.codeServer.connected}`);

        // Track the client
        inst.clients[key] = _.extend(inst.clients[key] || {}, { client: data.client });
        inst.clients[key] = _.defaults(inst.clients[key], { code: undefined });

        var response;

        if (inst.clients[key].code || !inst.codeServer.connected) {
          inst.log(`Sending cached code to client(${key}).`);
          response = _.defaults(inst.clients[key], {
            client: data.client,
            code: DefaultClientCode("No code server available")
          });

          network.emit(`code(${key})`, response);
        } else {
          // If there isn't any code yet, ask the code server. Any code
          // it sends back will be forwarded to clients automatically.
          inst.requestCode(data.client);
        }
      });
    });
  }

  requestCode(client) {
    // Request code from code server
    var key = getClientKey(client);
    this.log(`Requesting code for client(${key}) from code server.`);
    this.codeServer.emit('requestCode', { client: client });
  }
}

//
// Client Module
//
class LiveClientClient extends ClientModuleInterface {
  finishFadeOut() {
    network.removeListener(this.newCodeEvent, this.newCodeHandler);

    if (this.surface) {
      this.surface.destroy();
    }
  }

  willBeShownSoon(container, deadline) {
    this.surface = new CanvasSurface(container, wallGeometry);
    this.canvas = this.extendCanvas(this.surface.context);
    this.screen = { x: 0, y: 0, width: this.canvas.canvas.width, height: this.canvas.canvas.height };

    // Basic client info.
    this.client = {
          x: this.surface.virtualOffset.x,
          y: this.surface.virtualOffset.y,
    };

    // Use the default code until told otherwise.
    this.setClientCode({ code: "", controlled: false });

    // The event we listen to for new code.
    this.newCodeEvent = `code(${getClientKey(this.client)})`;
    network.on(this.newCodeEvent, this.setClientCode.bind(this));

    // Ask for some code to run.
    network.emit('requestCode', { client: this.client });
  }

  extendCanvas(canvas) {
    canvas._imageCache = {};

    canvas.writeText = function(x, y, text, style, font, extraProps) {
      _.extend(this, {textAlign: "left"}, extraProps);

      this.font = font || "120px Arial";
      this.fillStyle = style || "white";
      this.baseBaseline = "bottom";
      this.fillText(text, x, y);
    };

    canvas.draw = {
      image: function(x, y, url, scale) {
        scale = scale || 1;

        // Only load each image once since everything is done in the draw loop.
        var image = canvas._imageCache[url];

        if (!image) {
          image = new Image();
          image.src = url;
          canvas._imageCache[url] = image;
        }

        canvas.drawImage(image, x, y, scale * image.width, scale * image.height);
      },
      rectangle: function(rect, style) {
        var l = rect.left || rect.x;
        var t = rect.top || rect.y;
        var w = rect.width || rect.w;
        var h = rect.height|| rect.h;

        canvas.fillStyle = style || "white";
        canvas.fillRect(l, t, w, h);
      },
      circle: function(x, y, radius, style) {
        canvas.fillStyle = style || "white";
        canvas.beginPath();
        canvas.arc(x, y, radius, 0, 2 * Math.PI);
        canvas.fill();
        canvas.stroke();
      }
    };

    return canvas;
  }

  setClientCode(clientCode) {
    // Merge the new client code with the existing.
    this.clientCode = _.extend(this.clientCode || {}, clientCode, { time0: undefined });

    try {
      this.clientCode.draw = new Function('canvas', 'time', 'globalTime', 'screen', this.clientCode.code);
    } catch (e) {
      // If there is a syntax error "new Function" will fail, replace code with
      // error message.
      this.setClientCode({ code: ClientCodeError(e.message) });
    }
  }

  draw(time, delta) {
    this.canvas.draw.rectangle(this.screen, 'black');

    this.clientCode.time0 = this.clientCode.time0 || time;

    try {
      this.clientCode.draw(this.canvas, time - this.clientCode.time0, time, this.screen);
    } catch (e) {
      // If there is a runtime error, replace code with error message.
      this.setClientCode({ code: ClientCodeError(e.message) });
    }

    // Draw client info.
    this.canvas.writeText(10, this.screen.height-20, getClientKey(this.client), "white", "40px Arial");
    if (this.clientCode.controlled) {
      this.canvas.strokeStyle = HIGHLIGHT_COLORS[(this.client.x + this.client.y) % HIGHLIGHT_COLORS.length];
      this.canvas.lineWidth   = 10;
      this.canvas.strokeRect(0,0, this.screen.width, this.screen.height);
    }
  }
}

register(LiveClientServer, LiveClientClient);
<commit_msg>Handle default module code in server module.<commit_after>var _ = require('underscore');

//
// Confguration
//
//var CODE_SERVER = "http://130.211.117.91:3001";
var CODE_SERVER = "http://localhost:3001";

// Colors to highlight controlled clients.
var HIGHLIGHT_COLORS = ['#3cba54', '#f4c20d', '#db3236', '#4885ed'];

//
// Helper methods
//
function DefaultClientCode(client, text) {
  return (
`canvas.writeText(screen.width/2, screen.height/2-300, "Chicago Brick Live!", "#f4c20d", "140px Arial", {textAlign: "center"});
canvas.writeText(screen.width/2, screen.height/2-150, "${text}", "white", "100px Arial", {textAlign: "center"});
canvas.writeText(screen.width/2, screen.height/2+50, "${client.x}, ${client.y}", "white", "180px Arial", {textAlign: "center"});
canvas.draw.image(10, 10, "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", 0.5);`
  );
}

function ClientCodeError(message) {
  return (
`canvas.writeText(screen.width/2, screen.height/2-300, "Chicago Brick Live - Code Error", "#d50f25", "120px Arial", {textAlign: "center"});
canvas.writeText(screen.width/2, screen.height/2, "${message}", "white", "80px Arial", {textAlign: "center"});
canvas.draw.image(10, 10, "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", 0.5);`
  );
}

function getClientKey(client) {
  return `${client.x},${client.y}`;
}
//
// Server Module
//
class LiveClientServer extends ServerModuleInterface {
  constructor(config) {
    super();
    this.log = require('debug')('module:live_client');

    var inst = this;
    this.codeServer = require('socket.io-client')(CODE_SERVER);

    // All clients (x, y) that have ever connected.  Used to notify all clients
    // of global code changes (e.g., codeserver comes online) and cache code.
    this.clients = {};

    // Setup connection to code server.
    inst.codeServer.on('connect', function () {
      inst.log(`Connected to code server (${CODE_SERVER}).`);

      // When code server connection is made re-request code for all clients
      // we know about.
      // #TODO Is there any client list provided by brick?
      for (var key in inst.clients) {
        inst.requestCode(inst.clients[key].client);
      }
    });

    inst.codeServer.on('disconnect', function () {
      inst.log('Disconnected from code server.');
    });

    // TODO: These listeners are never removed, fix this.
    inst.codeServer.on('code', function(data) {
      // Make a unique key of the form 'x,y' so we can use a dictionary for clients.
      var key = getClientKey(data.client);
      inst.log(`Received new info for client(${key}).`);

      // Override empty code
      data.code = data.code || DefaultClientCode(data.client, "waiting for code...");

      // Cache the code in case the code server goes away.
      inst.clients[key] = data;

      // Forward code to clients.
      network.emit(`code(${key})`, data);
    });

    // Handle connections from clients.
    network.on('connection', function(socket) {
      socket.on('requestCode', function(data) {

        var key = getClientKey(data.client);
        inst.log(`Client(${key}) requested code.`);
        inst.log(`Code server connected: ${inst.codeServer.connected}`);

        // Track the client
        inst.clients[key] = _.extend(inst.clients[key] || {}, { client: data.client });
        inst.clients[key] = _.defaults(inst.clients[key], { code: undefined });

        var response;

        if (inst.clients[key].code || !inst.codeServer.connected) {
          inst.log(`Sending cached code to client(${key}).`);
          response = _.defaults(inst.clients[key], {
            client: data.client,
            code: DefaultClientCode(data.client, "No code server available")
          });

          network.emit(`code(${key})`, response);
        } else {
          // If there isn't any code yet, ask the code server. Any code
          // it sends back will be forwarded to clients automatically.
          inst.requestCode(data.client);
        }
      });
    });
  }

  requestCode(client) {
    // Request code from code server
    var key = getClientKey(client);
    this.log(`Requesting code for client(${key}) from code server.`);
    this.codeServer.emit('requestCode', { client: client });
  }
}

//
// Client Module
//
class LiveClientClient extends ClientModuleInterface {
  finishFadeOut() {
    network.removeListener(this.newCodeEvent, this.newCodeHandler);

    if (this.surface) {
      this.surface.destroy();
    }
  }

  willBeShownSoon(container, deadline) {
    this.surface = new CanvasSurface(container, wallGeometry);
    this.canvas = this.extendCanvas(this.surface.context);
    this.screen = { x: 0, y: 0, width: this.canvas.canvas.width, height: this.canvas.canvas.height };

    // Basic client info.
    this.client = {
          x: this.surface.virtualOffset.x,
          y: this.surface.virtualOffset.y,
    };

    // Use the default code until told otherwise.
    this.setClientCode({ code: "", controlled: false });

    // The event we listen to for new code.
    this.newCodeEvent = `code(${getClientKey(this.client)})`;
    network.on(this.newCodeEvent, this.setClientCode.bind(this));

    // Ask for some code to run.
    network.emit('requestCode', { client: this.client });
  }

  extendCanvas(canvas) {
    canvas._imageCache = {};

    canvas.writeText = function(x, y, text, style, font, extraProps) {
      _.extend(this, {textAlign: "left"}, extraProps);

      this.font = font || "120px Arial";
      this.fillStyle = style || "white";
      this.baseBaseline = "bottom";
      this.fillText(text, x, y);
    };

    canvas.draw = {
      image: function(x, y, url, scale) {
        scale = scale || 1;

        // Only load each image once since everything is done in the draw loop.
        var image = canvas._imageCache[url];

        if (!image) {
          image = new Image();
          image.src = url;
          canvas._imageCache[url] = image;
        }

        canvas.drawImage(image, x, y, scale * image.width, scale * image.height);
      },
      rectangle: function(rect, style) {
        var l = rect.left || rect.x;
        var t = rect.top || rect.y;
        var w = rect.width || rect.w;
        var h = rect.height|| rect.h;

        canvas.fillStyle = style || "white";
        canvas.fillRect(l, t, w, h);
      },
      circle: function(x, y, radius, style) {
        canvas.fillStyle = style || "white";
        canvas.beginPath();
        canvas.arc(x, y, radius, 0, 2 * Math.PI);
        canvas.fill();
        canvas.stroke();
      }
    };

    return canvas;
  }

  setClientCode(clientCode) {
    // Merge the new client code with the existing.
    this.clientCode = _.extend(this.clientCode || {}, clientCode, { time0: undefined });

    try {
      this.clientCode.draw = new Function('canvas', 'time', 'globalTime', 'screen', this.clientCode.code);
    } catch (e) {
      // If there is a syntax error "new Function" will fail, replace code with
      // error message.
      this.setClientCode({ code: ClientCodeError(e.message) });
    }
  }

  draw(time, delta) {
    this.canvas.draw.rectangle(this.screen, 'black');

    this.clientCode.time0 = this.clientCode.time0 || time;

    try {
      this.clientCode.draw(this.canvas, time - this.clientCode.time0, time, this.screen);
    } catch (e) {
      // If there is a runtime error, replace code with error message.
      this.setClientCode({ code: ClientCodeError(e.message) });
    }

    // Draw client info.
    this.canvas.writeText(10, this.screen.height-20, getClientKey(this.client), "white", "40px Arial");
    if (this.clientCode.controlled) {
      this.canvas.strokeStyle = HIGHLIGHT_COLORS[(this.client.x + this.client.y) % HIGHLIGHT_COLORS.length];
      this.canvas.lineWidth   = 10;
      this.canvas.strokeRect(0,0, this.screen.width, this.screen.height);
    }
  }
}

register(LiveClientServer, LiveClientClient);
<|endoftext|>


<commit_before>var nav = null;
var buttons = [];
var main = null;
var conversations = [];
var contacts = null;
var phone = null;
var message = null;
var message_number = null;
var last = null;
var state = 'idle';
var statusline = null;

var contact = {};

var number = null;

var token = null;
var connection = null;
var incoming = null;

var messageTime = new Date();

var xhr = function(method, resource, data, callback) {
	var req = new XMLHttpRequest();

	req.responseType = 'json';

	req.addEventListener('load', function(ev) {
		callback === undefined || callback(req.response);
	});

	req.open(method, resource);
	if (data === undefined) {
		req.send();
	}
	else {
		req.setRequestHeader('Content-Type', 'application/json');
		req.send(JSON.stringify(data));
	}
};

var load = function() {
	// get elements
	nav = document.getElementById('nav');
	buttons = [document.getElementById('button_contacts'), document.getElementById('button_message'), document.getElementById('button_phone')];
	main = document.getElementById('main');
	conversations = [];
	contacts = document.getElementById('contacts');
	phone = document.getElementById('phone');
	message = document.getElementById('message');
	message_number = document.getElementById('message_number');
	statusline = document.getElementById('status');

	contact = {};

	// load contacts
	xhr('get', '/contacts/', undefined, function(response) {
		contact = response;

		Object.keys(contact).forEach(function(key) {
			var li = document.createElement('li');

			var span_name = document.createElement('span');
			var span_number = document.createElement('span');
			var button_call = document.createElement('button');
			var button_message = document.createElement('button');

			span_name.innerText = contact[key];
			span_number.innerText = key;

			button_call.innerText = 'Call';
			button_message.innerText = 'Message';

			button_call.addEventListener('click', function(ev) {
				window.call(key);
			});
			button_message.addEventListener('click', function(ev) {
				window.open(key);
			});

			li.appendChild(span_name);
			li.appendChild(span_number);
			li.appendChild(button_call);
			li.appendChild(button_message);

			contacts.appendChild(li);
		});
	});

	// setup callbacks
	xhr('get', '/browser', undefined, function(data) {
		// get number
		number = data.number;

		// get token
		token = data.token;

		// setup Twilio.Device
		Twilio.Device.setup(token);

		Twilio.Device.connect(function (conn) {
			state = 'connected';
			statusline.innerText = 'Connected.';
		});

		Twilio.Device.disconnect(function (conn) {
			state = 'idle';
			statusline.innerText = 'Dial a Number';
		});

		Twilio.Device.incoming(function (conn) {
			state = 'incoming';

			incoming = conn;

			// get from name
			from = conn.parameters.From;

			if (from in contact)
				from = contact[from];

			statusline.innerText = 'Incoming Call From ' + from;

			// open phone
			select('phone');
		});

		Twilio.Device.offline(function(device) {
			// get another token
			xhr('get', '/browser', undefined, function(data) {
				// get token
				token = data.token;

				// setup Twilio.Device
				Twilio.Device.setup(token);
			});
		});

		// setup message callbacks
		var messageUpdate = function() {
			var currentTime = messageTime;
			var current = messageTime.toISOString();
			var nextTime = new Date();

			xhr('get', '/msgs/?date_sent_after=' + current + '&to=' + number, undefined, function(data) {
				data.forEach(function(message) {
					if ((new Date(message.date)) < nextTime)
						window.open(message.from, message);
				});
			});

			xhr('get', '/msgs/?date_sent_after=' + current + '&from=' + number, undefined, function(data) {
				data.forEach(function(message) {
					if ((new Date(message.date)) < nextTime)
						window.open(message.to, message);
				});
			});

			messageTime = nextTime;

			setTimeout(messageUpdate, 2000);
		};

		// initiate message updates
		messageUpdate();

		// setup ping callbacks
		var pingUpdate = function() {
			xhr('post', '/browser', {});

			setTimeout(pingUpdate, 5000);
		};

		// initiate ping updates
		pingUpdate();
	});

	// select nothing
	select(null);

	// show body
	document.body.style.display = 'flex';
};

var open = function(number, message) {
	var show = function(number) {
		// create new chat block
		var chat = document.createElement('div');
		chat.id = number;
		chat.classList.add('chat');
		chat.style.display = 'none';

		var container = document.createElement('div');

		var input = document.createElement('input');
		input.type = 'text';
		input.placeholder = 'Enter Message Here...';
		input.addEventListener('keyup', function(ev) {
			if (ev.keyCode === 13) {
				ev.preventDefault();
				xhr('post', '/msg', {'to': number, 'body': input.value});
				input.value = '';
			}
		});

		chat.appendChild(container);
		chat.appendChild(input);

		main.appendChild(chat);
		conversations.push(chat);

		// create button
		var button_container = document.createElement('div');
		button_container.id = 'nav_' + number;

		var button = document.createElement('button');
		button.id = 'button_' + number;
		if (number in contact)
			button.innerText = contact[number];
		else
			button.innerText = number;
		button.addEventListener('click', function(ev) { window.select(number) });

		buttons.push(button);

		var close = document.createElement('button');
		close.classList.add('close');
		close.innerText = '×';
		close.addEventListener('click', function(ev) { window.close(number) });

		button_container.appendChild(button);
		button_container.appendChild(close);

		nav.insertBefore(button_container, nav.firstChild);

		return container;
	}

	var write = function(container, number, message) {
		// create chat bubble
		var div = document.createElement('div');

		// set class based on whether this was sent or receieved
		if (message.from === number)
			div.classList.add('me');
		else
			div.classList.add('you');

		// get message time
		var date = new Date(message.date);

		// format time
		var time = document.createElement('time');
		time.innerText = '';
		time.innerText += date.getFullYear();
		time.innerText += '-';
		time.innerText += ('0' + (date.getMonth() + 1)).slice(-2);
		time.innerText += '-';
		time.innerText += ('0' + date.getDate()).slice(-2);
		time.innerText += ' ';
		time.innerText += ('0' + date.getHours()).slice(-2);
		time.innerText += ':';
		time.innerText += ('0' + date.getMinutes()).slice(-2);

		// add body
		var p = document.createElement('p');
		p.innerText = message.body;

		// join time and body into message
		div.appendChild(time);
		div.appendChild(p);

		// add message to chat window
		container.appendChild(div);
	}

	if (document.getElementById(number) === null) {
		// show number
		var container = show(number);

		// bring chat forward
		select(number);

		// load chat
		xhr('get', '/msgs/?to=' + number, undefined, function(data) {
			xhr('get', '/msgs/?from=' + number, undefined, function(dataInner) {
				// get all messages
				var messages = data.concat(dataInner);

				// sort by date
				messages.sort(function(left, right) {
					return new Date(left.date) - new Date(right.date);
				});

				// generate elements
				messages.forEach(function(message) {
					write(container, number, message);
				});

				// scroll chat down
				container.scrollTop = 2147483646;
			});
		});
	}
	else {
		// get container
		var container = document.getElementById(number).children[0];

		// bring chat forward
		select(number);

		// load given message
		if (message !== undefined)
			write(container, number, message);

		// scroll chat down
		container.scrollTop = 2147483646;
	}
};

var close = function(number) {
	var chat = document.getElementById(number);

	if (chat === null)
		return;

	// remove chat block
	main.removeChild(chat);
	conversations.splice(conversations.indexOf(chat), 1);

	var container = document.getElementById('nav_' + number);

	if (container === null)
		return;

	// remove button
	nav.removeChild(container);
	buttons.splice(buttons.indexOf(container.children[0]), 1);

	// bring last thing forward
	select(last);
};

var click = function(key) {
	if (key === 'message') {
		window.open(message_number.value);
		message_number.value = '';

		return;
	}

	if (state === 'connecting' || state === 'connected') {
		if (key === 'hangup') {
			hangup();
		}
		else if (key === 'dial') {
			// do nothing
		}
		else {
			connection.sendDigits(key);
		}
	}
	else if (state === 'idle') {
		if (key === 'hangup') {
			// do nothing
		}
		else if (key === 'dial') {
			// do nothing
		}
		else {
			statusline.innerText = key;
		}

		state = 'dialing';
	}
	else if (state === 'dialing') {
		if (key === 'dial') {
			call(statusline.innerText);
		}
		else if (key === 'hangup') {
			statusline.innerText = 'Dial a Number...';

			state = 'idle';
		}
		else {
			statusline.innerText += key;
		}
	}
	else if (state === 'incoming') {
		if (key === 'dial') {
			incoming.accept();

			connection = incoming;
		}
		else if (key === 'hangup') {
			incoming.reject();
		}

		state = 'connected';
		status.innerText = 'Connected.';
	}
};

var call = function(number) {
	// update state
	state = 'connecting';

	// create connection
	connection = Twilio.Device.connect({'To': number});

	// call number
	select('phone');
};

var hangup = function() {
	// close phone
	select(last);

	// disconnect line
	Twilio.Device.disconnectAll();
};

var select = function(id) {
	if (id === 'toggle_phone') {
		if (phone.style.display === 'none')
			// behave as though phone were selected
			id = 'phone';
		else
			// behave as though last were selected
			id = last;
	}
	else if (id === 'toggle_message') {
		if (message.style.display === 'none')
			// behave as though message were selected
			id = 'message';
		else
			// behave as though last were selected
			id = last;
	}

	if (id === 'phone') {
		// simply display phone
		phone.style.display = '';
		message.style.display = 'none';
	}
	else if (id === 'message') {
		// display message
		message.style.display = '';
		phone.style.display = 'none';
	}
	else {
		// close all conversations
		conversations.forEach(function(element) {
			element.style.display = 'none';
		});

		// close phone and contacts
		contacts.style.display = 'none';
		message.style.display = 'none';
		phone.style.display = 'none';

		// display requested section
		if (id !== null) {
			var target = document.getElementById(id);
			if (target !== null)
				target.style.display = '';
			else
				id = null;
		}
	}

	// disable all buttons
	buttons.forEach(function(element) {
		element.classList.remove('active');
	});

	// mark respective button as active
	if (id !== null)
		document.getElementById('button_' + id).classList.add('active');

	if (id !== 'phone' && id !== 'message')
		last = id;
};

window.addEventListener('load', load);
<commit_msg>attempt to only write messages once ever<commit_after>var nav = null;
var buttons = [];
var main = null;
var conversations = [];
var contacts = null;
var phone = null;
var message = null;
var message_number = null;
var last = null;
var state = 'idle';
var statusline = null;

var contact = {};

var number = null;

var token = null;
var connection = null;
var incoming = null;

var messageTime = new Date();

var xhr = function(method, resource, data, callback) {
	var req = new XMLHttpRequest();

	req.responseType = 'json';

	req.addEventListener('load', function(ev) {
		callback === undefined || callback(req.response);
	});

	req.open(method, resource);
	if (data === undefined) {
		req.send();
	}
	else {
		req.setRequestHeader('Content-Type', 'application/json');
		req.send(JSON.stringify(data));
	}
};

var load = function() {
	// get elements
	nav = document.getElementById('nav');
	buttons = [document.getElementById('button_contacts'), document.getElementById('button_message'), document.getElementById('button_phone')];
	main = document.getElementById('main');
	conversations = [];
	contacts = document.getElementById('contacts');
	phone = document.getElementById('phone');
	message = document.getElementById('message');
	message_number = document.getElementById('message_number');
	statusline = document.getElementById('status');

	contact = {};

	// load contacts
	xhr('get', '/contacts/', undefined, function(response) {
		contact = response;

		Object.keys(contact).forEach(function(key) {
			var li = document.createElement('li');

			var span_name = document.createElement('span');
			var span_number = document.createElement('span');
			var button_call = document.createElement('button');
			var button_message = document.createElement('button');

			span_name.innerText = contact[key];
			span_number.innerText = key;

			button_call.innerText = 'Call';
			button_message.innerText = 'Message';

			button_call.addEventListener('click', function(ev) {
				window.call(key);
			});
			button_message.addEventListener('click', function(ev) {
				window.open(key);
			});

			li.appendChild(span_name);
			li.appendChild(span_number);
			li.appendChild(button_call);
			li.appendChild(button_message);

			contacts.appendChild(li);
		});
	});

	// setup callbacks
	xhr('get', '/browser', undefined, function(data) {
		// get number
		number = data.number;

		// get token
		token = data.token;

		// setup Twilio.Device
		Twilio.Device.setup(token);

		Twilio.Device.connect(function (conn) {
			state = 'connected';
			statusline.innerText = 'Connected.';
		});

		Twilio.Device.disconnect(function (conn) {
			state = 'idle';
			statusline.innerText = 'Dial a Number';
		});

		Twilio.Device.incoming(function (conn) {
			state = 'incoming';

			incoming = conn;

			// get from name
			from = conn.parameters.From;

			if (from in contact)
				from = contact[from];

			statusline.innerText = 'Incoming Call From ' + from;

			// open phone
			select('phone');
		});

		Twilio.Device.offline(function(device) {
			// get another token
			xhr('get', '/browser', undefined, function(data) {
				// get token
				token = data.token;

				// setup Twilio.Device
				Twilio.Device.setup(token);
			});
		});

		// setup message callbacks
		var messageUpdate = function() {
			var currentTime = messageTime;
			var current = messageTime.toISOString();
			var nextTime = new Date();

			xhr('get', '/msgs/?date_sent_after=' + current + '&to=' + number, undefined, function(data) {
				data.forEach(function(message) {
					window.open(message.from, message);
				});
			});

			xhr('get', '/msgs/?date_sent_after=' + current + '&from=' + number, undefined, function(data) {
				data.forEach(function(message) {
					window.open(message.to, message);
				});
			});

			messageTime = nextTime;

			setTimeout(messageUpdate, 2000);
		};

		// initiate message updates
		messageUpdate();

		// setup ping callbacks
		var pingUpdate = function() {
			xhr('post', '/browser', {});

			setTimeout(pingUpdate, 5000);
		};

		// initiate ping updates
		pingUpdate();
	});

	// select nothing
	select(null);

	// show body
	document.body.style.display = 'flex';
};

var open = function(number, message) {
	var show = function(number) {
		// create new chat block
		var chat = document.createElement('div');
		chat.id = number;
		chat.classList.add('chat');
		chat.style.display = 'none';

		var container = document.createElement('div');

		var input = document.createElement('input');
		input.type = 'text';
		input.placeholder = 'Enter Message Here...';
		input.addEventListener('keyup', function(ev) {
			if (ev.keyCode === 13) {
				ev.preventDefault();
				xhr('post', '/msg', {'to': number, 'body': input.value});
				input.value = '';
			}
		});

		chat.appendChild(container);
		chat.appendChild(input);

		main.appendChild(chat);
		conversations.push(chat);

		// create button
		var button_container = document.createElement('div');
		button_container.id = 'nav_' + number;

		var button = document.createElement('button');
		button.id = 'button_' + number;
		if (number in contact)
			button.innerText = contact[number];
		else
			button.innerText = number;
		button.addEventListener('click', function(ev) { window.select(number) });

		buttons.push(button);

		var close = document.createElement('button');
		close.classList.add('close');
		close.innerText = '×';
		close.addEventListener('click', function(ev) { window.close(number) });

		button_container.appendChild(button);
		button_container.appendChild(close);

		nav.insertBefore(button_container, nav.firstChild);

		return container;
	}

	var write = function(container, number, message) {
		// prevent double writes
		if (document.getElementById(message.sid) !== null)
			return;

		// create chat bubble
		var div = document.createElement('div');
		div.id = message.sid;

		// set class based on whether this was sent or receieved
		if (message.from === number)
			div.classList.add('me');
		else
			div.classList.add('you');

		// get message time
		var date = new Date(message.date);

		// format time
		var time = document.createElement('time');
		time.innerText = '';
		time.innerText += date.getFullYear();
		time.innerText += '-';
		time.innerText += ('0' + (date.getMonth() + 1)).slice(-2);
		time.innerText += '-';
		time.innerText += ('0' + date.getDate()).slice(-2);
		time.innerText += ' ';
		time.innerText += ('0' + date.getHours()).slice(-2);
		time.innerText += ':';
		time.innerText += ('0' + date.getMinutes()).slice(-2);

		// add body
		var p = document.createElement('p');
		p.innerText = message.body;

		// join time and body into message
		div.appendChild(time);
		div.appendChild(p);

		// add message to chat window
		container.appendChild(div);
	}

	if (document.getElementById(number) === null) {
		// show number
		var container = show(number);

		// bring chat forward
		select(number);

		// load chat
		xhr('get', '/msgs/?to=' + number, undefined, function(data) {
			xhr('get', '/msgs/?from=' + number, undefined, function(dataInner) {
				// get all messages
				var messages = data.concat(dataInner);

				// sort by date
				messages.sort(function(left, right) {
					return new Date(left.date) - new Date(right.date);
				});

				// generate elements
				messages.forEach(function(message) {
					write(container, number, message);
				});

				// scroll chat down
				container.scrollTop = 2147483646;
			});
		});
	}
	else {
		// get container
		var container = document.getElementById(number).children[0];

		// bring chat forward
		select(number);

		// load given message
		if (message !== undefined)
			write(container, number, message);

		// scroll chat down
		container.scrollTop = 2147483646;
	}
};

var close = function(number) {
	var chat = document.getElementById(number);

	if (chat === null)
		return;

	// remove chat block
	main.removeChild(chat);
	conversations.splice(conversations.indexOf(chat), 1);

	var container = document.getElementById('nav_' + number);

	if (container === null)
		return;

	// remove button
	nav.removeChild(container);
	buttons.splice(buttons.indexOf(container.children[0]), 1);

	// bring last thing forward
	select(last);
};

var click = function(key) {
	if (key === 'message') {
		window.open(message_number.value);
		message_number.value = '';

		return;
	}

	if (state === 'connecting' || state === 'connected') {
		if (key === 'hangup') {
			hangup();
		}
		else if (key === 'dial') {
			// do nothing
		}
		else {
			connection.sendDigits(key);
		}
	}
	else if (state === 'idle') {
		if (key === 'hangup') {
			// do nothing
		}
		else if (key === 'dial') {
			// do nothing
		}
		else {
			statusline.innerText = key;
		}

		state = 'dialing';
	}
	else if (state === 'dialing') {
		if (key === 'dial') {
			call(statusline.innerText);
		}
		else if (key === 'hangup') {
			statusline.innerText = 'Dial a Number...';

			state = 'idle';
		}
		else {
			statusline.innerText += key;
		}
	}
	else if (state === 'incoming') {
		if (key === 'dial') {
			incoming.accept();

			connection = incoming;
		}
		else if (key === 'hangup') {
			incoming.reject();
		}

		state = 'connected';
		status.innerText = 'Connected.';
	}
};

var call = function(number) {
	// update state
	state = 'connecting';

	// create connection
	connection = Twilio.Device.connect({'To': number});

	// call number
	select('phone');
};

var hangup = function() {
	// close phone
	select(last);

	// disconnect line
	Twilio.Device.disconnectAll();
};

var select = function(id) {
	if (id === 'toggle_phone') {
		if (phone.style.display === 'none')
			// behave as though phone were selected
			id = 'phone';
		else
			// behave as though last were selected
			id = last;
	}
	else if (id === 'toggle_message') {
		if (message.style.display === 'none')
			// behave as though message were selected
			id = 'message';
		else
			// behave as though last were selected
			id = last;
	}

	if (id === 'phone') {
		// simply display phone
		phone.style.display = '';
		message.style.display = 'none';
	}
	else if (id === 'message') {
		// display message
		message.style.display = '';
		phone.style.display = 'none';
	}
	else {
		// close all conversations
		conversations.forEach(function(element) {
			element.style.display = 'none';
		});

		// close phone and contacts
		contacts.style.display = 'none';
		message.style.display = 'none';
		phone.style.display = 'none';

		// display requested section
		if (id !== null) {
			var target = document.getElementById(id);
			if (target !== null)
				target.style.display = '';
			else
				id = null;
		}
	}

	// disable all buttons
	buttons.forEach(function(element) {
		element.classList.remove('active');
	});

	// mark respective button as active
	if (id !== null)
		document.getElementById('button_' + id).classList.add('active');

	if (id !== 'phone' && id !== 'message')
		last = id;
};

window.addEventListener('load', load);
<|endoftext|>


<commit_before>addEvent(window, "load", sortables_init);

var SORT_COLUMN_INDEX;

function sortables_init() {
		var lastAssignedId = 0;
    // Find all tables with class sortable and make them sortable
    if (!document.getElementsByTagName) return;
    tbls = document.getElementsByTagName("table");
    for (ti=0;ti<tbls.length;ti++) {
        thisTbl = tbls[ti];
				if (!thisTbl.id) {
      		thisTbl.id = 'sortableTable'+(lastAssignedId++);
    		}
        if (((' '+thisTbl.className+' ').indexOf("sortable") != -1) && (thisTbl.id)) {
            //initTable(thisTbl.id);
            ts_makeSortable(thisTbl);
        }
    }
}

function ts_makeSortable(table) {
    if (table.rows && table.rows.length > 0) {
        var firstRow = table.rows[0];
    }
    if (!firstRow) return;

    // We have a first row: assume it's the header, and make its contents clickable links
    for (var i=0;i<firstRow.cells.length;i++) {
        var cell = firstRow.cells[i];
        var txt = ts_getInnerText(cell);
        cell.innerHTML = '' +
        txt+'   ';
    }
}

function ts_getInnerText(el) {
	if (typeof el == "string") return el;
	if (typeof el == "undefined") { return el };
	if (el.innerText) return el.innerText;	//Not needed but it is faster
	var str = "";

	var cs = el.childNodes;
	var l = cs.length;
	for (var i = 0; i < l; i++) {
		switch (cs[i].nodeType) {
			case 1: //ELEMENT_NODE
				str += ts_getInnerText(cs[i]);
				break;
			case 3:	//TEXT_NODE
				str += cs[i].nodeValue;
				break;
		}
	}
	return str;
}

function ts_resortTable(lnk,clid) {
    // get the span
    var span;
    for (var ci=0;ci<lnk.childNodes.length;ci++) {
        if (lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span') span = lnk.childNodes[ci];
    }
    var spantext = ts_getInnerText(span);
    var td = lnk.parentNode;
    while(td.nodeName.toLowerCase() != 'td')
        td = td.parentNode;
    var column = typeof(clid) == 'undefined' ? td.cellIndex : clid;
    var table = getParent(td,'TABLE');

    // Work out a type for the column
    if (table.rows.length <= 1) return;
    var itm = ts_getInnerText(table.rows[1].cells[column]);
    sortfn = ts_sort_caseinsensitive;
    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) sortfn = ts_sort_date;
    if (itm.match(/^\d\d[\/-]\S\S\S[\/-]\d\d\d\d$/)) sortfn = ts_sort_date;
    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) sortfn = ts_sort_date;
    if (itm.match(/^[$]/)) sortfn = ts_sort_currency;
    if (itm.match(/^[\d\.]+\s*(bytes|b|kb|tb|gb|mb)$/i)) sortfn = ts_sort_filesize;
    // Special cases for our mailbox lists
    if (itm.match(/^(Empty|Unlimited)$/)) sortfn = ts_sort_filesize;
    if (itm.match(/^[\d\.]+%?$/)) sortfn = ts_sort_numeric;
    if (itm.match(/^\d+\.\d+\.\d+\.\d+$/)) sortfn = ts_sort_ip;
    SORT_COLUMN_INDEX = column;
    var firstRow = new Array();
    var newRows = new Array();
    for (i=0;i<table.rows[0].length;i++) { firstRow[i] = table.rows[0][i]; }
    for (j=1;j<table.rows.length;j++) { newRows[j-1] = table.rows[j]; }

    newRows.sort(sortfn);

    if (span.getAttribute("sortdir") == 'down') {
        ARROW = '  ↑';
        newRows.reverse();
        span.setAttribute('sortdir','up');
    } else {
        ARROW = '  ↓';
        span.setAttribute('sortdir','down');
    }

    // We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
    // don't do sortbottom rows
    for (i=0;i<newRows.length;i++) { if (!newRows[i].className || (newRows[i].className && (newRows[i].className.indexOf('sortbottom') == -1))) table.tBodies[0].appendChild(newRows[i]);}
    // do sortbottom rows only
    for (i=0;i<newRows.length;i++) { if (newRows[i].className && (newRows[i].className.indexOf('sortbottom') != -1)) table.tBodies[0].appendChild(newRows[i]);}

    // Delete any other arrows there may be showing
    var allspans = document.getElementsByTagName("span");
    for (var ci=0;ci<allspans.length;ci++) {
        if (allspans[ci].className == 'sortarrow') {
            if (getParent(allspans[ci],"table") == getParent(lnk,"table")) { // in the same table as us?
                allspans[ci].innerHTML = '   ';
            }
        }
    }

    span.innerHTML = ARROW;
}

function getParent(el, pTagName) {
	if (el == null) return null;
	else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase())	// Gecko bug, supposed to be uppercase
		return el;
	else
		return getParent(el.parentNode, pTagName);
}
function ts_sort_date(a,b) {
    // y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
    if (aa.length == 10) {
        // yyyy/mm/dd format
        dt1 = aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);
    } else if (aa.length == 11) {
        // dd/mon/yyyy format
        dt1 = aa.substr(7,4)+ts_month_num(aa.substr(3,3))+aa.substr(0,2);
    } else {
        yr = aa.substr(6,2);
        if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
        dt1 = yr+aa.substr(3,2)+aa.substr(0,2);
    }
    if (bb.length == 10) {
        dt2 = bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);
    } else if (bb.length == 11) {
        // dd/mon/yyyy format
        dt2 = bb.substr(7,4)+ts_month_num(bb.substr(3,3))+bb.substr(0,2);
    } else {
        yr = bb.substr(6,2);
        if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
        dt2 = yr+bb.substr(3,2)+bb.substr(0,2);
    }
    if (dt1==dt2) return 0;
    if (dt1<dt2) return -1;
    return 1;
}

function ts_month_num(month) {
  month = month.toLowerCase();
  return month == 'jan' ? '01' :
	 month == 'feb' ? '02' :
	 month == 'mar' ? '03' :
	 month == 'apr' ? '04' :
	 month == 'may' ? '05' :
	 month == 'jun' ? '06' :
	 month == 'jul' ? '07' :
	 month == 'aug' ? '08' :
	 month == 'sep' ? '09' :
	 month == 'oct' ? '10' :
	 month == 'nov' ? '11' :
	 month == 'dec' ? '12' : '00';
}

function ts_sort_currency(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
    return parseFloat(aa) - parseFloat(bb);
}

// handles file sizes, simple numerics, and Unlimited/Empty/None special cases
function ts_sort_filesize(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();

    if (aa.length == 0) return -1;
    else if (bb.length == 0) return 1;

    var regex = /^([\d\.]*|none|empty|unlimited)\s*(bytes|b|kb|tb|gb|mb)?$/i;
    matchA = aa.match(regex);
    matchB = bb.match(regex);

    // Give file size class an integer value, if we don't already have one
    if (matchA[1] == 'none') valA = -999;
    else if (matchA[1] == 'empty') valA = 0;
    else if (matchA[1] == '0') valA = 0;
    else if (matchA[1] == 'unlimited') valA = 999;
    else if (matchA[2] == 'b' || matchA[2] == 'bytes') valA = 1;
    else if (matchA[2] == undefined || matchA[2] == '') valA = 1;
    else if (matchA[2] == 'kb') valA = 2;
    else if (matchA[2] == 'mb') valA = 3;
    else if (matchA[2] == 'gb') valA = 4;
    else if (matchA[2] == 'tb') valA = 5;

    if (matchB[1] == 'none') valB = -999;
    else if (matchB[1] == 'empty') valB = 0;
    else if (matchB[1] == '0') valB = 0;
    else if (matchB[1] == 'unlimited') valB = 999;
    else if (matchB[2] == 'b' || matchB[2] == 'bytes') valB = 1;
    else if (matchB[2] == undefined || matchB[2] == '') valB = 1;
    else if (matchB[2] == 'kb') valB = 2;
    else if (matchB[2] == 'mb') valB = 3;
    else if (matchB[2] == 'gb') valB = 4;
    else if (matchB[2] == 'tb') valB = 5;

    if (valA == valB) {
			if ( isNaN(matchA[1])) return -1;
      if ( isNaN(matchB[1])) return 1;
      // Files are in the same size class kb/gb/mb/etc
      // just do a numeric sort on the file size
			return matchA[1]-matchB[1];
    } else if (valA < valB) {
      return -1;
    } else if (valA > valB) {
      return 1;
    }
}

function ts_sort_numeric(a,b) {
    aa = parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
    if (isNaN(aa)) aa = 0;
    bb = parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));
    if (isNaN(bb)) bb = 0;
    return aa-bb;
}

function ts_sort_caseinsensitive(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
    if (aa==bb) return 0;
    if (aa<bb) return -1;
    return 1;
}

function ts_sort_ip(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
    var regexp = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
    var matchA = aa.match(regexp);
    var matchB = bb.match(regexp);
    return !matchA ? -1 :
	   !matchB ? 1 :
	   parseInt(matchA[1]) < parseInt(matchB[1]) ? -1 :
           parseInt(matchA[1]) > parseInt(matchB[1]) ? 1 :
	   parseInt(matchA[2]) < parseInt(matchB[2]) ? -1 :
           parseInt(matchA[2]) > parseInt(matchB[2]) ? 1 :
	   parseInt(matchA[3]) < parseInt(matchB[3]) ? -1 :
           parseInt(matchA[3]) > parseInt(matchB[3]) ? 1 :
	   parseInt(matchA[4]) < parseInt(matchB[4]) ? -1 :
           parseInt(matchA[4]) > parseInt(matchB[4]) ? 1 : 0;
}

function ts_sort_default(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
    if (aa==bb) return 0;
    if (aa<bb) return -1;
    return 1;
}

function addEvent(elm, evType, fn, useCapture)
// addEvent and removeEvent
// cross-browser event handling for IE5+,  NS6 and Mozilla
// By Scott Andrew
{
  if (elm.addEventListener){
    elm.addEventListener(evType, fn, useCapture);
    return true;
  } else if (elm.attachEvent){
    var r = elm.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be removed");
  }
}
<commit_msg>Update sorttable.js<commit_after>addEvent(window, "load", sortables_init);

var SORT_COLUMN_INDEX;

function sortables_init() {
     var lastAssignedId = 0;
    // Find all tables with class sortable and make them sortable
    if (!document.getElementsByTagName) return;
    tbls = document.getElementsByTagName("table");
    for (ti=0;ti<tbls.length;ti++) {
        thisTbl = tbls[ti];
	if (!thisTbl.id) {
      	    thisTbl.id = 'sortableTable'+(lastAssignedId++);
    	}
        if (((' '+thisTbl.className+' ').indexOf("sortable") != -1) && (thisTbl.id)) {
            //initTable(thisTbl.id);
            ts_makeSortable(thisTbl);
        }
    }
}

function ts_makeSortable(table) {
    if (table.rows && table.rows.length > 0) {
        var firstRow = table.rows[0];
    }
    if (!firstRow) return;

    // We have a first row: assume it's the header, and make its contents clickable links
    for (var i=0;i<firstRow.cells.length;i++) {
        var cell = firstRow.cells[i];
        var txt = ts_getInnerText(cell);
        cell.innerHTML = '' +
        txt+'   ';
    }
}

function ts_getInnerText(el) {
	if (typeof el == "string") return el;
	if (typeof el == "undefined") { return el };
	if (el.innerText) return el.innerText;	//Not needed but it is faster
	var str = "";

	var cs = el.childNodes;
	var l = cs.length;
	for (var i = 0; i < l; i++) {
		switch (cs[i].nodeType) {
			case 1: //ELEMENT_NODE
				str += ts_getInnerText(cs[i]);
				break;
			case 3:	//TEXT_NODE
				str += cs[i].nodeValue;
				break;
		}
	}
	return str;
}

function ts_resortTable(lnk,clid) {
    // get the span
    var span;
    for (var ci=0;ci<lnk.childNodes.length;ci++) {
        if (lnk.childNodes[ci].tagName && lnk.childNodes[ci].tagName.toLowerCase() == 'span') span = lnk.childNodes[ci];
    }
    var spantext = ts_getInnerText(span);
    var td = lnk.parentNode;
    while(td.nodeName.toLowerCase() != 'td')
        td = td.parentNode;
    var column = typeof(clid) == 'undefined' ? td.cellIndex : clid;
    var table = getParent(td,'TABLE');

    // Work out a type for the column
    if (table.rows.length <= 1) return;
    var itm = ts_getInnerText(table.rows[1].cells[column]);
    sortfn = ts_sort_caseinsensitive;
    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/)) sortfn = ts_sort_date;
    if (itm.match(/^\d\d[\/-]\S\S\S[\/-]\d\d\d\d$/)) sortfn = ts_sort_date;
    if (itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/)) sortfn = ts_sort_date;
    if (itm.match(/^[£$]/)) sortfn = ts_sort_currency;
    if (itm.match(/^[\d\.]+\s*(bytes|b|kb|tb|gb|mb)$/i)) sortfn = ts_sort_filesize;
    // Special cases for our mailbox lists
    if (itm.match(/^(Empty|Unlimited)$/)) sortfn = ts_sort_filesize;
    if (itm.match(/^[\d\.]+%?$/)) sortfn = ts_sort_numeric;
    if (itm.match(/^\d+\.\d+\.\d+\.\d+$/)) sortfn = ts_sort_ip;
    SORT_COLUMN_INDEX = column;
    var firstRow = new Array();
    var newRows = new Array();
    for (i=0;i<table.rows[0].length;i++) { firstRow[i] = table.rows[0][i]; }
    for (j=1;j<table.rows.length;j++) { newRows[j-1] = table.rows[j]; }

    newRows.sort(sortfn);

    if (span.getAttribute("sortdir") == 'down') {
        ARROW = '  ↑';
        newRows.reverse();
        span.setAttribute('sortdir','up');
    } else {
        ARROW = '  ↓';
        span.setAttribute('sortdir','down');
    }

    // We appendChild rows that already exist to the tbody, so it moves them rather than creating new ones
    // don't do sortbottom rows
    for (i=0;i<newRows.length;i++) { if (!newRows[i].className || (newRows[i].className && (newRows[i].className.indexOf('sortbottom') == -1))) table.tBodies[0].appendChild(newRows[i]);}
    // do sortbottom rows only
    for (i=0;i<newRows.length;i++) { if (newRows[i].className && (newRows[i].className.indexOf('sortbottom') != -1)) table.tBodies[0].appendChild(newRows[i]);}

    // Delete any other arrows there may be showing
    var allspans = document.getElementsByTagName("span");
    for (var ci=0;ci<allspans.length;ci++) {
        if (allspans[ci].className == 'sortarrow') {
            if (getParent(allspans[ci],"table") == getParent(lnk,"table")) { // in the same table as us?
                allspans[ci].innerHTML = '   ';
            }
        }
    }

    span.innerHTML = ARROW;
}

function getParent(el, pTagName) {
	if (el == null) return null;
	else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase())	// Gecko bug, supposed to be uppercase
		return el;
	else
		return getParent(el.parentNode, pTagName);
}
function ts_sort_date(a,b) {
    // y2k notes: two digit years less than 50 are treated as 20XX, greater than 50 are treated as 19XX
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
    if (aa.length == 10) {
        // yyyy/mm/dd format
        dt1 = aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);
    } else if (aa.length == 11) {
        // dd/mon/yyyy format
        dt1 = aa.substr(7,4)+ts_month_num(aa.substr(3,3))+aa.substr(0,2);
    } else {
        yr = aa.substr(6,2);
        if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
        dt1 = yr+aa.substr(3,2)+aa.substr(0,2);
    }
    if (bb.length == 10) {
        dt2 = bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);
    } else if (bb.length == 11) {
        // dd/mon/yyyy format
        dt2 = bb.substr(7,4)+ts_month_num(bb.substr(3,3))+bb.substr(0,2);
    } else {
        yr = bb.substr(6,2);
        if (parseInt(yr) < 50) { yr = '20'+yr; } else { yr = '19'+yr; }
        dt2 = yr+bb.substr(3,2)+bb.substr(0,2);
    }
    if (dt1==dt2) return 0;
    if (dt1<dt2) return -1;
    return 1;
}

function ts_month_num(month) {
  month = month.toLowerCase();
  return month == 'jan' ? '01' :
	 month == 'feb' ? '02' :
	 month == 'mar' ? '03' :
	 month == 'apr' ? '04' :
	 month == 'may' ? '05' :
	 month == 'jun' ? '06' :
	 month == 'jul' ? '07' :
	 month == 'aug' ? '08' :
	 month == 'sep' ? '09' :
	 month == 'oct' ? '10' :
	 month == 'nov' ? '11' :
	 month == 'dec' ? '12' : '00';
}

function ts_sort_currency(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');
    return parseFloat(aa) - parseFloat(bb);
}

// handles file sizes, simple numerics, and Unlimited/Empty/None special cases
function ts_sort_filesize(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();

    if (aa.length == 0) return -1;
    else if (bb.length == 0) return 1;

    var regex = /^([\d\.]*|none|empty|unlimited)\s*(bytes|b|kb|tb|gb|mb)?$/i;
    matchA = aa.match(regex);
    matchB = bb.match(regex);

    // Give file size class an integer value, if we don't already have one
    if (matchA[1] == 'none') valA = -999;
    else if (matchA[1] == 'empty') valA = 0;
    else if (matchA[1] == '0') valA = 0;
    else if (matchA[1] == 'unlimited') valA = 999;
    else if (matchA[2] == 'b' || matchA[2] == 'bytes') valA = 1;
    else if (matchA[2] == undefined || matchA[2] == '') valA = 1;
    else if (matchA[2] == 'kb') valA = 2;
    else if (matchA[2] == 'mb') valA = 3;
    else if (matchA[2] == 'gb') valA = 4;
    else if (matchA[2] == 'tb') valA = 5;

    if (matchB[1] == 'none') valB = -999;
    else if (matchB[1] == 'empty') valB = 0;
    else if (matchB[1] == '0') valB = 0;
    else if (matchB[1] == 'unlimited') valB = 999;
    else if (matchB[2] == 'b' || matchB[2] == 'bytes') valB = 1;
    else if (matchB[2] == undefined || matchB[2] == '') valB = 1;
    else if (matchB[2] == 'kb') valB = 2;
    else if (matchB[2] == 'mb') valB = 3;
    else if (matchB[2] == 'gb') valB = 4;
    else if (matchB[2] == 'tb') valB = 5;

    if (valA == valB) {
			if ( isNaN(matchA[1])) return -1;
      if ( isNaN(matchB[1])) return 1;
      // Files are in the same size class kb/gb/mb/etc
      // just do a numeric sort on the file size
			return matchA[1]-matchB[1];
    } else if (valA < valB) {
      return -1;
    } else if (valA > valB) {
      return 1;
    }
}

function ts_sort_numeric(a,b) {
    aa = parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));
    if (isNaN(aa)) aa = 0;
    bb = parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));
    if (isNaN(bb)) bb = 0;
    return aa-bb;
}

function ts_sort_caseinsensitive(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
    if (aa==bb) return 0;
    if (aa<bb) return -1;
    return 1;
}

function ts_sort_ip(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();
    var regexp = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
    var matchA = aa.match(regexp);
    var matchB = bb.match(regexp);
    return !matchA ? -1 :
	   !matchB ? 1 :
	   parseInt(matchA[1]) < parseInt(matchB[1]) ? -1 :
           parseInt(matchA[1]) > parseInt(matchB[1]) ? 1 :
	   parseInt(matchA[2]) < parseInt(matchB[2]) ? -1 :
           parseInt(matchA[2]) > parseInt(matchB[2]) ? 1 :
	   parseInt(matchA[3]) < parseInt(matchB[3]) ? -1 :
           parseInt(matchA[3]) > parseInt(matchB[3]) ? 1 :
	   parseInt(matchA[4]) < parseInt(matchB[4]) ? -1 :
           parseInt(matchA[4]) > parseInt(matchB[4]) ? 1 : 0;
}

function ts_sort_default(a,b) {
    aa = ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);
    bb = ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);
    if (aa==bb) return 0;
    if (aa<bb) return -1;
    return 1;
}

function addEvent(elm, evType, fn, useCapture)
// addEvent and removeEvent
// cross-browser event handling for IE5+,  NS6 and Mozilla
// By Scott Andrew
{
  if (elm.addEventListener){
    elm.addEventListener(evType, fn, useCapture);
    return true;
  } else if (elm.attachEvent){
    var r = elm.attachEvent("on"+evType, fn);
    return r;
  } else {
    alert("Handler could not be removed");
  }
}
<|endoftext|>


<commit_before>var gems = angular.module('gems');

gems.controller('queryController', function($scope, $http){

    $scope.fields = [];

    $scope.operators = [
        {name: 'Less than', operator: 'lt'},
        {name: 'Less than or equal to', operator: 'lte'},
        {name: 'Greater than', operator: 'gt'},
        {name: 'Greater than or equal to', operator: 'gte'},
        {name: 'Equal to', operator: 'eq'},
        {name: 'Text contains', operator: 'co'},
        {name: 'Not equal to', operator: 'neq'},
        {name: 'Text does not contain', operator: 'nco'},
        {name: 'Text is exactly', operator: 'ex'}
    ];

    $scope.queryWords = '';

    $scope.queryWordOperator = function queryWordOperator(op){
        var retVal = '';

        if(op == 'lt'){
            retVal = '<';
        } else if(op == 'lte'){
            retVal = '<=';
        } else if(op == 'gt'){
            retVal = '>';
        } else if(op == 'gte'){
            retVal = '>=';
        } else if(op == 'eq' || op == 'ex'){
            retVal = 'equals';
        } else if(op == 'co'){
            retVal = 'contains';
        } else if(op == 'neq'){
            retVal = 'not equals';
        } else if(op == 'nco'){
            retVal = 'not contains';
        } else {
            retVal = 'None Selected';
        }

        return retVal;
    };

    $scope.fetchFields = function fetchFields(){
        $http.get('/get_unique_keys/')
            .success(function(data){
                $scope.fields = data;
                $scope.columns = $scope.fields;
                $scope.processFilters();
            })
    };

    $scope.addFieldFilter = function addFieldFilter(pIndex){
        $scope.filters[pIndex].filters.push({loperator: 'and', operator: null, value: null});
    };

    $scope.removeFieldFilter = function removeFieldFilter(pIndex, index){
        $scope.filters[pIndex].filters.splice(index, 1);
    };

    $scope.removeFilter = function removeFilter(pIndex){
        $scope.filters.splice(pIndex, 1);
    }

    $scope.processFilters = function processFilters(){
        for(var x = 0; x < $scope.filters.length; ++x){
            for(var y = 0; y < $scope.fields.length; ++y){
                if($scope.filters[x].field !== null && $scope.filters[x].field.name == $scope.fields[y].name){
                    $scope.filters[x].field = $scope.fields[y];
                }
            }
        }
    };

    $scope.makeQueryWords = function makeQueryWords(){
        var words = '';

        for(var x = 0; x < $scope.filters.length; ++x){
            var filter = $scope.filters[x];
            var field = filter.field;

            if(x > 0){
                words += ' ' + filter.loperator + ' ';
            }

            if(filter.filters.length > 1){
                words += '( ';
            }

            for(var y = 0; y < filter.filters.length; ++y){
                if(y > 0){
                    words += ' ' + filter.filters[y].loperator + ' ';
                }
                if(field != null && filter.filters[y].value != null){
                    words += field.name + ' ' + $scope.queryWordOperator(filter.filters[y].operator) + ' ' + filter.filters[y].value;
                }
            }

            if(filter.filters.length > 1){
                words += ' )';
            }
        }

        $scope.queryWords = words;

        $scope.setQueryWords($scope.queryWords);
    };

    $scope.validateFilter = function validateFilter(){
        var valid = true;

        if($scope.filters == null || $scope.filters.length == 0){
            valid = false;
        } else {
            for(var x = 0; x < $scope.filters.length; ++x){
                var filter = $scope.filters[x];
                var field = filter.field;

                if(field == null){
                    valid = false;
                    break;
                }

                for(var y = 0; y < filter.filters.length; ++y){
                    if(y > 0 && filter.filters[y].loperator == null){
                        valid = false;
                        break;
                    }

                    if(filter.filters[y].value == null || filter.filters[y].operator == null){
                        valid = false;
                        break;
                    }
                }
            }
        }

        return valid;
    };

    $scope.fetchFields();
    $scope.makeQueryWords();

    $scope.$watch('filters', function(){
        $scope.makeQueryWords();
        $scope.setQueryValid($scope.validateFilter());
    }, true);
});<commit_msg>Fixed a bug with the validateFilter function<commit_after>var gems = angular.module('gems');

gems.controller('queryController', function($scope, $http){

    $scope.fields = [];

    $scope.operators = [
        {name: 'Less than', operator: 'lt'},
        {name: 'Less than or equal to', operator: 'lte'},
        {name: 'Greater than', operator: 'gt'},
        {name: 'Greater than or equal to', operator: 'gte'},
        {name: 'Equal to', operator: 'eq'},
        {name: 'Text contains', operator: 'co'},
        {name: 'Not equal to', operator: 'neq'},
        {name: 'Text does not contain', operator: 'nco'},
        {name: 'Text is exactly', operator: 'ex'}
    ];

    $scope.queryWords = '';

    $scope.queryWordOperator = function queryWordOperator(op){
        var retVal = '';

        if(op == 'lt'){
            retVal = '<';
        } else if(op == 'lte'){
            retVal = '<=';
        } else if(op == 'gt'){
            retVal = '>';
        } else if(op == 'gte'){
            retVal = '>=';
        } else if(op == 'eq' || op == 'ex'){
            retVal = 'equals';
        } else if(op == 'co'){
            retVal = 'contains';
        } else if(op == 'neq'){
            retVal = 'not equals';
        } else if(op == 'nco'){
            retVal = 'not contains';
        } else {
            retVal = 'None Selected';
        }

        return retVal;
    };

    $scope.fetchFields = function fetchFields(){
        $http.get('/get_unique_keys/')
            .success(function(data){
                $scope.fields = data;
                $scope.columns = $scope.fields;
                $scope.processFilters();
            })
    };

    $scope.addFieldFilter = function addFieldFilter(pIndex){
        $scope.filters[pIndex].filters.push({loperator: 'and', operator: null, value: null});
    };

    $scope.removeFieldFilter = function removeFieldFilter(pIndex, index){
        $scope.filters[pIndex].filters.splice(index, 1);
    };

    $scope.removeFilter = function removeFilter(pIndex){
        $scope.filters.splice(pIndex, 1);
    }

    $scope.processFilters = function processFilters(){
        for(var x = 0; x < $scope.filters.length; ++x){
            for(var y = 0; y < $scope.fields.length; ++y){
                if($scope.filters[x].field !== null && $scope.filters[x].field.name == $scope.fields[y].name){
                    $scope.filters[x].field = $scope.fields[y];
                }
            }
        }
    };

    $scope.makeQueryWords = function makeQueryWords(){
        var words = '';

        for(var x = 0; x < $scope.filters.length; ++x){
            var filter = $scope.filters[x];
            var field = filter.field;

            if(x > 0){
                words += ' ' + filter.loperator + ' ';
            }

            if(filter.filters.length > 1){
                words += '( ';
            }

            for(var y = 0; y < filter.filters.length; ++y){
                if(y > 0){
                    words += ' ' + filter.filters[y].loperator + ' ';
                }
                if(field != null && filter.filters[y].value != null){
                    words += field.name + ' ' + $scope.queryWordOperator(filter.filters[y].operator) + ' ' + filter.filters[y].value;
                }
            }

            if(filter.filters.length > 1){
                words += ' )';
            }
        }

        $scope.queryWords = words;

        $scope.setQueryWords($scope.queryWords);
    };

    $scope.validateFilter = function validateFilter(){
        var valid = true;

        if($scope.filters == null || $scope.filters.length == 0){
            valid = false;
        } else {
            for(var x = 0; x < $scope.filters.length; ++x){
                var filter = $scope.filters[x];
                var field = filter.field;

                if(field == null){
                    valid = false;
                    break;
                }

                for(var y = 0; y < filter.filters.length; ++y){
                    if(y > 0 && filter.filters[y].loperator == null){
                        valid = false;
                        break;
                    }

                    if(
                        typeof(filter.filters[y].value) === "undefined"
                        || filter.filters[y].value == null
                        || filter.filters[y].value === ""
                        || filter.filters[y].operator == null
                        ){
                        valid = false;
                        break;
                    }
                }
            }
        }

        return valid;
    };

    $scope.fetchFields();
    $scope.makeQueryWords();

    $scope.$watch('filters', function(){
        $scope.makeQueryWords();
        $scope.setQueryValid($scope.validateFilter());
    }, true);
});<|endoftext|>


<commit_before>/* eslint-disable max-len */
import React from 'react'

export const LOGGED_OUT_PROMOTIONS = [
  {
    username: 'kolotusha',
    caption:
Humans over algorithms.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1271818/ello-regular-eced431c.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834638/ello-hdpi-71521c0e.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834638/ello-xhdpi-71521c0e.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834638/ello-optimized-71521c0e.jpg' },
    },
  },
  {
    username: 'danclarke',
    caption:
Humans over algorithms.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/4631/ello-regular-31d9a8c4.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834637/ello-hdpi-83633f88.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834637/ello-xhdpi-83633f88.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834637/ello-optimized-83633f88.jpg' },
    },
  },
  {
    username: 'samrodriguezart',
    caption:
Welcome to the future.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2540242/ello-regular-f8d75deb.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834635/ello-hdpi-16a1a080.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834635/ello-xhdpi-16a1a080.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834635/ello-optimized-16a1a080.jpg' },
    },
  },
  {
    username: 'elenaeper',
    caption:
Welcome to the future.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2805911/ello-regular-fb28623e.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834634/ello-hdpi-f723b4f8.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834634/ello-xhdpi-f723b4f8.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834634/ello-optimized-f723b4f8.jpg' },
    },
  },
  {
    username: 'dervinbatarlo',
    caption:
Open source by default.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1887397/ello-regular-c1d6240a.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834633/ello-hdpi-2667d4e3.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834633/ello-xhdpi-2667d4e3.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834633/ello-optimized-2667d4e3.jpg' },
    },
  },
  {
    username: 'sydneysie',
    caption:
Open source by default.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2466011/ello-regular-38aea972.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834632/ello-hdpi-517f98bb.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834632/ello-xhdpi-517f98bb.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834632/ello-optimized-517f98bb.jpg' },
    },
  },
  {
    username: 'isiope',
    caption:
Creators. Community. Connection.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2160982/ello-regular-0b32ef1b.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834630/ello-hdpi-877fc6a5.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834630/ello-xhdpi-877fc6a5.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834630/ello-optimized-877fc6a5.jpg' },
    },
  },
  {
    username: 'sh-i',
    caption:
Creators. Community. Connection.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/9335/ello-regular-5044772f.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834627/ello-hdpi-5e1d7ecb.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834627/ello-xhdpi-5e1d7ecb.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834627/ello-optimized-5e1d7ecb.jpg' },
    },
  },
  {
    username: 'wrdbnr',
    caption:
Supporting creators since 2014.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2805926/ello-regular-03bd4eeb.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834625/ello-hdpi-329fab3f.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834625/ello-xhdpi-329fab3f.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834625/ello-optimized-329fab3f.jpg' },
    },
  },
  {
    username: 'woostaar',
    caption:
Supporting creators since 2014.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/235714/ello-regular-18013c26.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834622/ello-hdpi-53df2e66.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834622/ello-xhdpi-53df2e66.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834622/ello-optimized-53df2e66.jpg' },
    },
  },
  {
    username: 'ming_nomchong',
    caption:
Always ad-free.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2790069/ello-regular-286b5003.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834621/ello-hdpi-8cda840b.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834621/ello-xhdpi-8cda840b.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834621/ello-optimized-8cda840b.jpg' },
    },
  },
  {
    username: 'alconic',
    caption:
Always ad-free.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/12526/ello-regular-7d3b38c3.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834619/ello-hdpi-336dfa4f.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834619/ello-xhdpi-336dfa4f.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834619/ello-optimized-336dfa4f.jpg' },
    },
  },
  {
    username: 'phigun',
    caption:
Realize the promise of the internet.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/746013/ello-regular-72f5aac0.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834616/ello-hdpi-9e880ef7.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834616/ello-xhdpi-9e880ef7.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834616/ello-optimized-9e880ef7.jpg' },
    },
  },
  {
    username: 'sammearns',
    caption:
Realize the promise of the internet.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/737620/ello-regular-3a7c2232.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834614/ello-hdpi-125703ff.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834614/ello-xhdpi-125703ff.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834614/ello-optimized-125703ff.jpg' },
    },
  },
  {
    username: 'l0ll3',
    caption:
Explore. Expand. Escape.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2489935/ello-regular-342c211d.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834612/ello-hdpi-f51a0259.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834612/ello-xhdpi-f51a0259.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834612/ello-optimized-f51a0259.jpg' },
    },
  },
  {
    username: 'bensusi',
    caption:
Explore. Expand. Escape.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2793217/ello-regular-3179f0f9.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834611/ello-hdpi-49df0c43.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834611/ello-xhdpi-49df0c43.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834611/ello-optimized-49df0c43.jpg' },
    },
  },
  {
    username: 'scottlistfield',
    caption:
Adventurers and explorers wanted.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2033987/ello-regular-cad17f34.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834610/ello-hdpi-defdc55f.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834610/ello-xhdpi-defdc55f.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834610/ello-optimized-defdc55f.jpg' },
    },
  },
  {
    username: 'robincerutti',
    caption:
Adventurers and explorers wanted.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/225302/ello-regular-78805a22.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834609/ello-hdpi-1bfeea09.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834609/ello-xhdpi-1bfeea09.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834609/ello-optimized-1bfeea09.jpg' },
    },
  },
  {
    username: 'p5art',
    caption:
He. She. It. They. We.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2181535/ello-regular-6ec6b0ea.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834607/ello-hdpi-755741fe.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834607/ello-xhdpi-755741fe.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834607/ello-optimized-755741fe.jpg' },
    },
  },
  {
    username: '29thfloor',
    caption:
He. She. It. They. We.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1215021/ello-regular-99b03214.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834604/ello-hdpi-5f3e8ccb.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3834604/ello-xhdpi-5f3e8ccb.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3834604/ello-optimized-5f3e8ccb.jpg' },
    },
  },
]
<commit_msg>Update Logged Out Banner images and copy<commit_after>/* eslint-disable max-len */
import React from 'react'

export const LOGGED_OUT_PROMOTIONS = [
  {
    username: 'build',
    caption:
The Creators Network.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/5087/ello-regular-9faf1585.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900706/ello-hdpi-96077cb6.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900706/ello-xhdpi-96077cb6.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900706/ello-optimized-96077cb6.jpg' },
    },
  },
  {
    username: 'damien',
    caption:
The Creators Network.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/14994/ello-regular-d2efd436.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900705/ello-hdpi-0ae2dc09.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900705/ello-xhdpi-0ae2dc09.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900705/ello-optimized-0ae2dc09.jpg' },
    },
  },
  {
    username: 'wongping',
    caption:
The Creators Network.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/2548149/ello-regular-7ff4fc73.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900704/ello-hdpi-dcb02cb4.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900704/ello-xhdpi-dcb02cb4.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900704/ello-optimized-dcb02cb4.jpg' },
    },
  },
  {
    username: 'gohmn',
    caption:
The Creators Network.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1038700/ello-regular-430985e2.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900703/ello-hdpi-6a2ca778.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900703/ello-xhdpi-6a2ca778.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900703/ello-optimized-6a2ca778.jpg' },
    },
  },
  {
    username: 'spoonmagazine',
    caption:
The Creators Network.
,
    avatar: {
      regular: { url: 'https://assets.ello.co/uploads/user/avatar/1007137/ello-regular-a5ad0f7e.png' },
    },
    coverImage: {
      hdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900701/ello-hdpi-caeda4dd.jpg' },
      xhdpi: { url: 'https://assets.ello.co/uploads/asset/attachment/3900701/ello-xhdpi-caeda4dd.jpg' },
      optimized: { url: 'https://assets.ello.co/uploads/asset/attachment/3900701/ello-optimized-caeda4dd.jpg' },
    },
  },
]
<|endoftext|>


<commit_before>/* eslint-disable react/no-multi-comp */
import Immutable from 'immutable'
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { getCategories } from '../actions/discover'
import { saveProfile } from '../actions/profile'
import { selectCreatorTypeCategories } from '../selectors/categories'
import { selectCreatorTypeCategoryIds } from '../selectors/profile'
import { css, hover, media, modifier, parent, select } from '../styles/jss'
import { ONBOARDING_VERSION } from '../constants/application_types'
import { closeModal } from '../actions/modals'
import FormButton from '../components/forms/FormButton'
import CreatorTypesModal from '../components/modals/CreatorTypesModal'
import * as s from '../styles/jso'

const containerStyle = css(
  s.fullWidth,
  { maxWidth: 490 },
)

const headerStyle = css(
  s.colorA,
  s.sansRegular,
  s.fontSize24,
  s.mb20,
  s.transitionHeight,
)

const catHeaderStyle = css(
  { ...headerStyle },
  { marginTop: 30 },
  parent('.inSettings', s.mt20),
)

const buttonStyle = css(
  s.bgcWhite,
  s.borderA,
  s.center,
  s.colorA,
  s.mr10,
  s.px5,
  s.truncate,
  {
    borderRadius: 5,
    height: 40,
    maxWidth: 230,
    transition: 'background-color 0.2s, border-color 0.2s, color 0.2s',
    width: 'calc(50% - 5px)',
  },
  hover(s.colorWhite, s.bgcBlack, s.borderBlack),
  modifier('.isActive', s.colorWhite, s.bgcBlack, s.borderBlack),
  select(':nth-child(2n)', s.mr0),
)

const catButtonStyle = css(
  { ...buttonStyle },
  modifier('.isActive', hover(s.bgc6, { border: '1px solid #666' })),
  media('(min-width: 26.25em)', // 420 / 16 = 26.25em
    { maxWidth: 150, width: 'calc(33% - 6px)' },
    select(':nth-child(2n)', s.mr10),
    select(':nth-child(3n)', s.mr0),
  ),
)

const submitButtonStyle = css(
  s.mt30,
  s.hv40,
  { lineHeight: 3 },
)

export class CategoryButton extends PureComponent {

  static propTypes = {
    category: PropTypes.object.isRequired,
    isActive: PropTypes.bool.isRequired,
    onCategoryClick: PropTypes.func.isRequired,
  }

  componentWillMount() {
    this.state = { isActive: this.props.isActive }
  }

  onClick = () => {
    const { category, onCategoryClick } = this.props
    this.setState({ isActive: !this.state.isActive })
    onCategoryClick(Number(category.get('id')))
  }

  render() {
    const { category } = this.props
    const { isActive } = this.state
    return (
      <button
        className={`${catButtonStyle} ${isActive ? 'isActive' : ''}`}
        onClick={this.onClick}
      >
        {category.get('name')}
      </button>
    )
  }
}

function mapStateToProps(state) {
  return {
    categories: selectCreatorTypeCategories(state),
    creatorTypeIds: (selectCreatorTypeCategoryIds(state) || Immutable.List()).toArray(),
  }
}

class CreatorTypeContainer extends PureComponent {

  static propTypes = {
    categories: PropTypes.array.isRequired,
    classModifier: PropTypes.string,
    creatorTypeIds: PropTypes.array,
    dispatch: PropTypes.func.isRequired,
  }

  static defaultProps = {
    classModifier: '',
    creatorTypeIds: [],
  }

  componentWillMount() {
    const { classModifier, creatorTypeIds, dispatch } = this.props
    this.state = {
      artistActive: creatorTypeIds.length > 0,
      categoryIds: creatorTypeIds,
      fanActive: creatorTypeIds.length === 0 && classModifier === 'inSettings',
    }
    dispatch(getCategories())
  }

  onCategoryClick = (id) => {
    const ids = [...this.state.categoryIds]
    const index = ids.indexOf(id)
    if (index === -1) {
      ids.push(id)
    } else {
      ids.splice(index, 1)
    }
    this.setState({ categoryIds: ids })
  }

  onClickArtist = () => {
    this.setState({ artistActive: true, fanActive: false })
  }

  onClickFan = () => {
    this.setState({
      artistActive: false,
      categoryIds: [],
      fanActive: true,
    })
  }

  onClickModalSubmit = () => {
    const { dispatch } = this.props
    const { categoryIds } = this.state
    dispatch(
      saveProfile({ creator_type_category_ids: categoryIds,
        web_onboarding_version: ONBOARDING_VERSION }))
    dispatch(closeModal(<CreatorTypesModal />))
  }

  render() {
    const { categories, classModifier } = this.props
    const { artistActive, categoryIds, fanActive } = this.state
    const showSubmit = classModifier !== 'inOnboarding' && classModifier !== 'inSettings' && (fanActive || categoryIds.length > 0)
    return (
      <div className={`${classModifier} ${containerStyle}`}>

I am here as:

          <button
            className={`${buttonStyle} ${artistActive ? 'isActive' : ''}`}
            disabled={artistActive}
            onClick={this.onClickArtist}
          >
            An Artist
          </button>
          <button
            className={`${buttonStyle} ${fanActive ? 'isActive' : ''}`}
            disabled={fanActive}
            onClick={this.onClickFan}
          >
            A Fan
          </button>

        {artistActive &&

I make:

              {categories.map(cat => (
                <CategoryButton
                  category={cat}
                  isActive={categoryIds.indexOf(Number(cat.get('id'))) > -1}
                  key={`category_${cat.get('id')}`}
                  onCategoryClick={this.onCategoryClick}
                />
              ))}

        }
        { showSubmit ? <FormButton
          className={`FormButton Submit isRounded ${submitButtonStyle}`}
          onClick={this.onClickModalSubmit}
        >
          Submit
        </FormButton> : null }

    )
  }
}

export default connect(mapStateToProps)(CreatorTypeContainer)

<commit_msg>modals - creator type - add bottom margin to category buttons<commit_after>/* eslint-disable react/no-multi-comp */
import Immutable from 'immutable'
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { getCategories } from '../actions/discover'
import { saveProfile } from '../actions/profile'
import { selectCreatorTypeCategories } from '../selectors/categories'
import { selectCreatorTypeCategoryIds } from '../selectors/profile'
import { css, hover, media, modifier, parent, select } from '../styles/jss'
import { ONBOARDING_VERSION } from '../constants/application_types'
import { closeModal } from '../actions/modals'
import FormButton from '../components/forms/FormButton'
import CreatorTypesModal from '../components/modals/CreatorTypesModal'
import * as s from '../styles/jso'

const containerStyle = css(
  s.fullWidth,
  { maxWidth: 490 },
)

const headerStyle = css(
  s.colorA,
  s.sansRegular,
  s.fontSize24,
  s.mb20,
  s.transitionHeight,
)

const catHeaderStyle = css(
  { ...headerStyle },
  { marginTop: 30 },
  parent('.inSettings', s.mt20),
)

const categoriesStyle = css(
  { marginBottom: -10 },
)

const buttonStyle = css(
  s.bgcWhite,
  s.borderA,
  s.center,
  s.colorA,
  s.mr10,
  s.px5,
  s.truncate,
  {
    borderRadius: 5,
    height: 40,
    maxWidth: 230,
    transition: 'background-color 0.2s, border-color 0.2s, color 0.2s',
    width: 'calc(50% - 5px)',
  },
  hover(s.colorWhite, s.bgcBlack, s.borderBlack),
  modifier('.isActive', s.colorWhite, s.bgcBlack, s.borderBlack),
  select(':nth-child(2n)', s.mr0),
)

const catButtonStyle = css(
  { ...buttonStyle },
  s.mb10,
  modifier('.isActive', hover(s.bgc6, { border: '1px solid #666' })),
  media('(min-width: 26.25em)', // 420 / 16 = 26.25em
    { maxWidth: 150, width: 'calc(33% - 6px)' },
    select(':nth-child(2n)', s.mr10),
    select(':nth-child(3n)', s.mr0),
  ),
)

const submitButtonStyle = css(
  s.mt30,
  s.hv40,
  { lineHeight: 3 },
)

export class CategoryButton extends PureComponent {

  static propTypes = {
    category: PropTypes.object.isRequired,
    isActive: PropTypes.bool.isRequired,
    onCategoryClick: PropTypes.func.isRequired,
  }

  componentWillMount() {
    this.state = { isActive: this.props.isActive }
  }

  onClick = () => {
    const { category, onCategoryClick } = this.props
    this.setState({ isActive: !this.state.isActive })
    onCategoryClick(Number(category.get('id')))
  }

  render() {
    const { category } = this.props
    const { isActive } = this.state
    return (
      <button
        className={`${catButtonStyle} ${isActive ? 'isActive' : ''}`}
        onClick={this.onClick}
      >
        {category.get('name')}
      </button>
    )
  }
}

function mapStateToProps(state) {
  return {
    categories: selectCreatorTypeCategories(state),
    creatorTypeIds: (selectCreatorTypeCategoryIds(state) || Immutable.List()).toArray(),
  }
}

class CreatorTypeContainer extends PureComponent {

  static propTypes = {
    categories: PropTypes.array.isRequired,
    classModifier: PropTypes.string,
    creatorTypeIds: PropTypes.array,
    dispatch: PropTypes.func.isRequired,
  }

  static defaultProps = {
    classModifier: '',
    creatorTypeIds: [],
  }

  componentWillMount() {
    const { classModifier, creatorTypeIds, dispatch } = this.props
    this.state = {
      artistActive: creatorTypeIds.length > 0,
      categoryIds: creatorTypeIds,
      fanActive: creatorTypeIds.length === 0 && classModifier === 'inSettings',
    }
    dispatch(getCategories())
  }

  onCategoryClick = (id) => {
    const ids = [...this.state.categoryIds]
    const index = ids.indexOf(id)
    if (index === -1) {
      ids.push(id)
    } else {
      ids.splice(index, 1)
    }
    this.setState({ categoryIds: ids })
  }

  onClickArtist = () => {
    this.setState({ artistActive: true, fanActive: false })
  }

  onClickFan = () => {
    this.setState({
      artistActive: false,
      categoryIds: [],
      fanActive: true,
    })
  }

  onClickModalSubmit = () => {
    const { dispatch } = this.props
    const { categoryIds } = this.state
    dispatch(
      saveProfile({ creator_type_category_ids: categoryIds,
        web_onboarding_version: ONBOARDING_VERSION }))
    dispatch(closeModal(<CreatorTypesModal />))
  }

  render() {
    const { categories, classModifier } = this.props
    const { artistActive, categoryIds, fanActive } = this.state
    const showSubmit = classModifier !== 'inOnboarding' && classModifier !== 'inSettings' && (fanActive || categoryIds.length > 0)
    return (
      <div className={`${classModifier} ${containerStyle}`}>

I am here as:

          <button
            className={`${buttonStyle} ${artistActive ? 'isActive' : ''}`}
            disabled={artistActive}
            onClick={this.onClickArtist}
          >
            An Artist
          </button>
          <button
            className={`${buttonStyle} ${fanActive ? 'isActive' : ''}`}
            disabled={fanActive}
            onClick={this.onClickFan}
          >
            A Fan
          </button>

        {artistActive &&

I make:

              {categories.map(cat => (
                <CategoryButton
                  category={cat}
                  isActive={categoryIds.indexOf(Number(cat.get('id'))) > -1}
                  key={`category_${cat.get('id')}`}
                  onCategoryClick={this.onCategoryClick}
                />
              ))}

        }
        { showSubmit ? <FormButton
          className={`FormButton Submit isRounded ${submitButtonStyle}`}
          onClick={this.onClickModalSubmit}
        >
          Submit
        </FormButton> : null }

    )
  }
}

export default connect(mapStateToProps)(CreatorTypeContainer)

<|endoftext|>