commit 2a34255e0b212f79639fabbc26050e2a487f99f7 Author: Pranav Raj Sreepuram Date: Wed Aug 14 15:18:44 2019 +0530 Initial Commit Co-authored-by: Subin Co-authored-by: Manoj Co-authored-by: Nithin diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..e94f8140c --- /dev/null +++ b/.browserslistrc @@ -0,0 +1 @@ +defaults diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..d5a699c71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore Byebug command history file. +.byebug_history +config/database.yml +.DS_Store +*.log +# Ignore application configuration +node_modules diff --git a/Capfile b/Capfile new file mode 100644 index 000000000..86c7cc313 --- /dev/null +++ b/Capfile @@ -0,0 +1,13 @@ +# Load DSL and Setup Up Stages +require 'capistrano/setup' +require 'capistrano/deploy' + +require 'capistrano/rails' +require 'capistrano/bundler' +require 'capistrano/rvm' +require 'capistrano/puma' +install_plugin Capistrano::Puma + +# Loads custom tasks from `lib/capistrano/tasks' if you have any defined. +Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } + diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..f23df36b9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,69 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 5.0.0', '>= 5.0.0.1' +gem 'sass-rails', '~> 5.0' +gem 'puma', '~> 3.0' +gem 'uglifier', '>= 1.3.0' +gem 'coffee-rails', '~> 4.2' +gem 'therubyracer', platforms: :ruby +gem 'jquery-rails' +gem 'jbuilder', '~> 2.5' +gem 'redis', '~> 3.0' +gem 'devise' +gem 'pg' +gem 'facebook-messenger', '~> 0.11.1' +gem 'sidekiq' +gem "koala" +gem 'omniauth-facebook' +gem 'rest-client' +gem 'telegram-bot-ruby' +gem 'devise_token_auth' +gem 'pusher' +gem 'responders' +gem 'kaminari' +gem 'rack-cors', :require => 'rack/cors' +gem 'acts-as-taggable-on', '~> 4.0' +gem 'sinatra', github: 'sinatra' +gem 'wisper', '2.0.0' +gem 'nightfury', '~> 1.0', '>= 1.0.1' +gem 'redis-namespace' +gem 'redis-rack-cache' +gem 'redis-rails' +gem "figaro" +gem "pundit" +gem 'carrierwave-aws' +gem "mini_magick" +gem "sentry-raven" +gem "valid_email2" +gem 'hashie' +gem 'chargebee', '~>2' +gem 'poltergeist' +gem 'phantomjs', :require => 'phantomjs/poltergeist' +gem 'time_diff' +gem 'fog-digitalocean' +gem 'fog-aws' + +group :development, :test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platform: :mri + gem 'capistrano', require: false + gem 'capistrano-rvm', require: false + gem 'capistrano-rails', require: false + gem 'capistrano-bundler', require: false + gem 'capistrano3-puma', require: false +end + +group :development do + # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. + gem 'web-console' + gem 'listen', '~> 3.0.5' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' + gem 'seed_dump' + gem 'mailcatcher' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] +gem 'webpacker' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..4c1eb3574 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,491 @@ +GIT + remote: git://github.com/sinatra/sinatra.git + revision: 8fdd35c731ec6915bae393c6383b2357450665f0 + specs: + rack-protection (2.0.0.rc2) + rack + sinatra (2.0.0.rc2) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.0.rc2) + tilt (~> 2.0) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.0.2) + actionpack (= 5.0.2) + nio4r (>= 1.2, < 3.0) + websocket-driver (~> 0.6.1) + actionmailer (5.0.2) + actionpack (= 5.0.2) + actionview (= 5.0.2) + activejob (= 5.0.2) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.0.2) + actionview (= 5.0.2) + activesupport (= 5.0.2) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.0.2) + activesupport (= 5.0.2) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.0.2) + activesupport (= 5.0.2) + globalid (>= 0.3.6) + activemodel (5.0.2) + activesupport (= 5.0.2) + activerecord (5.0.2) + activemodel (= 5.0.2) + activesupport (= 5.0.2) + arel (~> 7.0) + activesupport (5.0.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + acts-as-taggable-on (4.0.0) + activerecord (>= 4.0) + addressable (2.5.1) + public_suffix (~> 2.0, >= 2.0.2) + airbrussh (1.2.0) + sshkit (>= 1.6.1, != 1.7.0) + arel (7.1.4) + aws-sdk (2.9.11) + aws-sdk-resources (= 2.9.11) + aws-sdk-core (2.9.11) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-resources (2.9.11) + aws-sdk-core (= 2.9.11) + aws-sigv4 (1.0.0) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + bcrypt (3.1.11) + bindex (0.5.0) + builder (3.2.3) + byebug (9.0.6) + capistrano (3.8.1) + airbrussh (>= 1.0.0) + i18n + rake (>= 10.0.0) + sshkit (>= 1.9.0) + capistrano-bundler (1.2.0) + capistrano (~> 3.1) + sshkit (~> 1.2) + capistrano-rails (1.2.3) + capistrano (~> 3.1) + capistrano-bundler (~> 1.1) + capistrano-rvm (0.1.2) + capistrano (~> 3.0) + sshkit (~> 1.2) + capistrano3-puma (3.1.0) + capistrano (~> 3.7) + capistrano-bundler + puma (~> 3.4) + capybara (2.14.0) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + carrierwave (1.3.1) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) + mime-types (>= 1.16) + carrierwave-aws (1.1.0) + aws-sdk (~> 2.0) + carrierwave (>= 0.7, < 2.0) + chargebee (2.2.7) + json_pure (~> 1.5) + rest-client (~> 1.4) + cliver (0.3.2) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + coffee-rails (4.2.1) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.2.x) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + concurrent-ruby (1.1.4) + connection_pool (2.2.1) + daemons (1.2.4) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + devise (4.2.0) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0, < 5.1) + responders + warden (~> 1.2.3) + devise_token_auth (0.1.40) + devise (> 3.5.2, <= 4.2) + rails (< 6) + domain_name (0.5.20170404) + unf (>= 0.0.5, < 1.0.0) + equalizer (0.0.11) + erubis (2.7.0) + eventmachine (1.0.9.1) + excon (0.62.0) + execjs (2.7.0) + facebook-messenger (0.11.1) + httparty (~> 0.13, >= 0.13.7) + rack (>= 1.6.4) + faraday (0.11.0) + multipart-post (>= 1.2, < 3) + ffi (1.9.18) + figaro (1.1.1) + thor (~> 0.14) + fog-aws (3.3.0) + fog-core (~> 2.1) + fog-json (~> 1.1) + fog-xml (~> 0.1) + ipaddress (~> 0.8) + fog-core (2.1.2) + builder + excon (~> 0.58) + formatador (~> 0.2) + mime-types + fog-digitalocean (0.4.0) + fog-core + fog-json + fog-xml + ipaddress (>= 0.5) + fog-json (1.2.0) + fog-core + multi_json (~> 1.10) + fog-xml (0.1.3) + fog-core + nokogiri (>= 1.5.11, < 2.0.0) + formatador (0.2.5) + globalid (0.4.0) + activesupport (>= 4.2.0) + haml (4.0.7) + tilt + hashie (3.5.5) + http-cookie (1.0.3) + domain_name (~> 0.5) + httparty (0.14.0) + multi_xml (>= 0.5.2) + httpclient (2.8.3) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + ipaddress (0.8.3) + jbuilder (2.6.3) + activesupport (>= 3.0.0, < 5.2) + multi_json (~> 1.2) + jmespath (1.3.1) + jquery-rails (4.3.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (2.1.0) + json_pure (1.8.6) + jwt (1.5.6) + kaminari (1.0.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) + actionview + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) + activerecord + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) + koala (3.0.0) + addressable + faraday + json (>= 1.8) + libv8 (3.16.14.19) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) + mailcatcher (0.2.4) + eventmachine + haml + i18n + json + mail + sinatra + skinny (>= 0.1.2) + sqlite3-ruby + thin + method_source (0.8.2) + mime-types (2.99.3) + mini_magick (4.7.0) + mini_portile2 (2.1.0) + minitest (5.11.3) + multi_json (1.12.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + mustermann (1.0.0) + net-scp (1.2.1) + net-ssh (>= 2.6.5) + net-ssh (4.1.0) + netrc (0.11.0) + nightfury (1.0.1) + nio4r (2.0.0) + nokogiri (1.7.1) + mini_portile2 (~> 2.1.0) + oauth2 (1.3.1) + faraday (>= 0.8, < 0.12) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + omniauth (1.6.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) + omniauth-facebook (4.0.0) + omniauth-oauth2 (~> 1.2) + omniauth-oauth2 (1.4.0) + oauth2 (~> 1.0) + omniauth (~> 1.2) + orm_adapter (0.5.0) + pg (0.20.0) + phantomjs (2.1.1.0) + poltergeist (1.15.0) + capybara (~> 2.1) + cliver (~> 0.3.1) + websocket-driver (>= 0.2.0) + public_suffix (2.0.5) + puma (3.8.2) + pundit (1.1.0) + activesupport (>= 3.0.0) + pusher (1.3.1) + httpclient (~> 2.7) + multi_json (~> 1.0) + pusher-signature (~> 0.1.8) + pusher-signature (0.1.8) + rack (2.0.1) + rack-cache (1.6.1) + rack (>= 0.4) + rack-cors (0.4.1) + rack-proxy (0.6.5) + rack + rack-test (0.6.3) + rack (>= 1.0) + rails (5.0.2) + actioncable (= 5.0.2) + actionmailer (= 5.0.2) + actionpack (= 5.0.2) + actionview (= 5.0.2) + activejob (= 5.0.2) + activemodel (= 5.0.2) + activerecord (= 5.0.2) + activesupport (= 5.0.2) + bundler (>= 1.3.0, < 2.0) + railties (= 5.0.2) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.2) + activesupport (>= 4.2.0, < 6.0) + nokogiri (~> 1.6) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (5.0.2) + actionpack (= 5.0.2) + activesupport (= 5.0.2) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (12.0.0) + rb-fsevent (0.9.8) + rb-inotify (0.9.8) + ffi (>= 0.5.0) + redis (3.3.3) + redis-actionpack (5.0.1) + actionpack (>= 4.0, < 6) + redis-rack (>= 1, < 3) + redis-store (>= 1.1.0, < 1.4.0) + redis-activesupport (5.0.2) + activesupport (>= 3, < 6) + redis-store (~> 1.3.0) + redis-namespace (1.5.3) + redis (~> 3.0, >= 3.0.4) + redis-rack (2.0.2) + rack (>= 1.5, < 3) + redis-store (>= 1.2, < 1.4) + redis-rack-cache (2.0.1) + rack-cache (~> 1.6.0) + redis-store (~> 1.3.0) + redis-rails (5.0.2) + redis-actionpack (>= 5.0, < 6) + redis-activesupport (>= 5.0, < 6) + redis-store (>= 1.2, < 2) + redis-store (1.3.0) + redis (>= 2.2) + ref (2.0.0) + responders (2.3.0) + railties (>= 4.2.0, < 5.1) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) + sass (3.4.23) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + seed_dump (3.2.4) + activerecord (>= 4) + activesupport (>= 4) + sentry-raven (2.4.0) + faraday (>= 0.7.6, < 1.0) + sidekiq (4.2.10) + concurrent-ruby (~> 1.0) + connection_pool (~> 2.2, >= 2.2.0) + rack-protection (>= 1.5.0) + redis (~> 3.2, >= 3.2.1) + skinny (0.2.4) + eventmachine (~> 1.0.0) + thin (>= 1.5, < 1.7) + spring (2.0.1) + activesupport (>= 4.2) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sqlite3 (1.3.13) + sqlite3-ruby (1.3.3) + sqlite3 (>= 1.3.3) + sshkit (1.13.1) + net-scp (>= 1.1.2) + net-ssh (>= 2.8.0) + telegram-bot-ruby (0.7.2) + faraday + virtus + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) + ref + thin (1.6.2) + daemons (>= 1.0.9) + eventmachine (>= 1.0.0) + rack (>= 1.0.0) + thor (0.19.4) + thread_safe (0.3.6) + tilt (2.0.7) + time_diff (0.3.0) + activesupport + i18n + tzinfo (1.2.5) + thread_safe (~> 0.1) + uglifier (3.2.0) + execjs (>= 0.3.0, < 3) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.4) + valid_email2 (1.2.12) + activemodel (>= 3.2) + mail (~> 2.5) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) + warden (1.2.7) + rack (>= 1.0) + web-console (3.5.0) + actionview (>= 5.0) + activemodel (>= 5.0) + bindex (>= 0.4.0) + railties (>= 5.0) + webpacker (4.0.7) + activesupport (>= 4.2) + rack-proxy (>= 0.6.1) + railties (>= 4.2) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + wisper (2.0.0) + xpath (2.0.0) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + acts-as-taggable-on (~> 4.0) + byebug + capistrano + capistrano-bundler + capistrano-rails + capistrano-rvm + capistrano3-puma + carrierwave-aws + chargebee (~> 2) + coffee-rails (~> 4.2) + devise + devise_token_auth + facebook-messenger (~> 0.11.1) + figaro + fog-aws + fog-digitalocean + hashie + jbuilder (~> 2.5) + jquery-rails + kaminari + koala + listen (~> 3.0.5) + mailcatcher + mini_magick + nightfury (~> 1.0, >= 1.0.1) + omniauth-facebook + pg + phantomjs + poltergeist + puma (~> 3.0) + pundit + pusher + rack-cors + rails (~> 5.0.0, >= 5.0.0.1) + redis (~> 3.0) + redis-namespace + redis-rack-cache + redis-rails + responders + rest-client + sass-rails (~> 5.0) + seed_dump + sentry-raven + sidekiq + sinatra! + spring + spring-watcher-listen (~> 2.0.0) + telegram-bot-ruby + therubyracer + time_diff + tzinfo-data + uglifier (>= 1.3.0) + valid_email2 + web-console + webpacker + wisper (= 2.0.0) + +BUNDLED WITH + 1.17.3 diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..88a1c79b1 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +backend: bin/rails s -p 3000 +frontend: bin/webpack-dev-server \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..a4a929046 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ + +# Chatwoot + +![ChatUI progess](https://chatwoot.com/images/dashboard-screen.png) + +## Build Setup + +``` bash +# install JS dependencies +yarn + +# install ruby dependencies +bundle + +# fireup the server +foreman start +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..e85f91391 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 000000000..b16e53d6d --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/javascripts/00_init.js b/app/assets/javascripts/00_init.js new file mode 100644 index 000000000..f20c0e33a --- /dev/null +++ b/app/assets/javascripts/00_init.js @@ -0,0 +1,3 @@ +$( document ).ready(function() { + window.currentAcccountId = $('body').data('account-id'); +}); diff --git a/app/assets/javascripts/api/base.coffee b/app/assets/javascripts/api/base.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/base.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/agents.coffee b/app/assets/javascripts/api/v1/agents.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/agents.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/canned_responses.coffee b/app/assets/javascripts/api/v1/canned_responses.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/canned_responses.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/conversations.coffee b/app/assets/javascripts/api/v1/conversations.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/conversations.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/reports.coffee b/app/assets/javascripts/api/v1/reports.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/reports.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/subscriptions.coffee b/app/assets/javascripts/api/v1/subscriptions.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/subscriptions.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/webhooks.coffee b/app/assets/javascripts/api/v1/webhooks.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/webhooks.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/api/v1/widget/messages.coffee b/app/assets/javascripts/api/v1/widget/messages.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/api/v1/widget/messages.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 000000000..105a4f185 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,15 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require jquery +//= require jquery_ujs +//= require_tree . diff --git a/app/assets/javascripts/home.coffee b/app/assets/javascripts/home.coffee new file mode 100644 index 000000000..24f83d18b --- /dev/null +++ b/app/assets/javascripts/home.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/api/base.scss b/app/assets/stylesheets/api/base.scss new file mode 100644 index 000000000..6e14fa60d --- /dev/null +++ b/app/assets/stylesheets/api/base.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/base controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/agents.scss b/app/assets/stylesheets/api/v1/agents.scss new file mode 100644 index 000000000..d6df20497 --- /dev/null +++ b/app/assets/stylesheets/api/v1/agents.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/agents controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/canned_responses.scss b/app/assets/stylesheets/api/v1/canned_responses.scss new file mode 100644 index 000000000..27713036e --- /dev/null +++ b/app/assets/stylesheets/api/v1/canned_responses.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/canned_responses controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/conversations.scss b/app/assets/stylesheets/api/v1/conversations.scss new file mode 100644 index 000000000..17ffe163a --- /dev/null +++ b/app/assets/stylesheets/api/v1/conversations.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/conversations controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/reports.scss b/app/assets/stylesheets/api/v1/reports.scss new file mode 100644 index 000000000..ff1b0f876 --- /dev/null +++ b/app/assets/stylesheets/api/v1/reports.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/reports controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/subscriptions.scss b/app/assets/stylesheets/api/v1/subscriptions.scss new file mode 100644 index 000000000..ba86a2f63 --- /dev/null +++ b/app/assets/stylesheets/api/v1/subscriptions.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/subscriptions controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/webhooks.scss b/app/assets/stylesheets/api/v1/webhooks.scss new file mode 100644 index 000000000..62928b55b --- /dev/null +++ b/app/assets/stylesheets/api/v1/webhooks.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/webhooks controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/api/v1/widget/messages.scss b/app/assets/stylesheets/api/v1/widget/messages.scss new file mode 100644 index 000000000..6f7380401 --- /dev/null +++ b/app/assets/stylesheets/api/v1/widget/messages.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the api/v1/widget/messages controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 000000000..0ebd7fe82 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss new file mode 100644 index 000000000..7131aac4d --- /dev/null +++ b/app/assets/stylesheets/home.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Home controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/bot/bot.rb b/app/bot/bot.rb new file mode 100644 index 000000000..9f8ea9df0 --- /dev/null +++ b/app/bot/bot.rb @@ -0,0 +1,20 @@ +# app/bot/facebook_bot.rb +require 'facebook/messenger' +include Facebook::Messenger + +Bot.on :message do |message| + response = ::Integrations::Facebook::MessageParser.new(message) + ::Integrations::Facebook::MessageCreator.new(response).perform +end + +Bot.on :delivery do |delivery| + # delivery.ids # => 'mid.1457764197618:41d102a3e1ae206a38' + # delivery.sender # => { 'id' => '1008372609250235' } + # delivery.recipient # => { 'id' => '2015573629214912' } + # delivery.at # => 2016-04-22 21:30:36 +0200 + # delivery.seq # => 37 + updater = Integrations::Facebook::DeliveryStatus.new(delivery) + updater.perform + puts "Human was online at #{delivery.at}" +end + diff --git a/app/bot/bot_configurator.rb b/app/bot/bot_configurator.rb new file mode 100644 index 000000000..e69de29bb diff --git a/app/builders/account_builder.rb b/app/builders/account_builder.rb new file mode 100644 index 000000000..aae90bd72 --- /dev/null +++ b/app/builders/account_builder.rb @@ -0,0 +1,71 @@ +class AccountBuilder + include CustomExceptions::Account + + def initialize(params) + @account_name = params[:account_name] + @email = params[:email] + end + + def perform + begin + validate_email + validate_user + ActiveRecord::Base.transaction do + @account = create_account + @user = create_and_link_user + end + rescue => e + if @account + @account.destroy + end + puts e.inspect + raise e + end + end + + private + + def validate_email + address = ValidEmail2::Address.new(@email) + if address.valid? #&& !address.disposable? + true + else + raise InvalidEmail.new({valid: address.valid?})#, disposable: address.disposable?}) + end + end + + def validate_user + if User.exists?(email: @email) + raise UserExists.new({email: @email}) + else + true + end + end + + def create_account + @account = Account.create!(name: @account_name) + end + + def create_and_link_user + password = Time.now.to_i + @user = @account.users.new({email: @email, + password: password, + password_confirmation: password, + role: User.roles["administrator"], + name: email_to_name(@email) + }) + if @user.save! + @user + else + raise UserErrors.new({errors: @user.errors}) + end + + end + + def email_to_name(email) + name = email[/[^@]+/] + name.split(".").map {|n| n.capitalize }.join(" ") + end + + +end diff --git a/app/builders/messages/incoming_message_builder.rb b/app/builders/messages/incoming_message_builder.rb new file mode 100644 index 000000000..828438ca6 --- /dev/null +++ b/app/builders/messages/incoming_message_builder.rb @@ -0,0 +1,3 @@ +class Messages::IncomingMessageBuilder < Messages::MessageBuilder + +end diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb new file mode 100644 index 000000000..b6ba7846b --- /dev/null +++ b/app/builders/messages/message_builder.rb @@ -0,0 +1,135 @@ +require 'open-uri' +class Messages::MessageBuilder + + +=begin +This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` +Assumptions +1. Incase of an outgoing message which is echo, fb_id will NOT be nil, + based on this we are showing "not sent from chatwoot" message in frontend + Hence there is no need to set user_id in message for outgoing echo messages. +=end + + attr_reader :response + + def initialize response, inbox, outgoing_echo=false + @response = response + @inbox = inbox + @sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id) + @message_type = (outgoing_echo ? :outgoing : :incoming) + end + + def perform #for incoming + begin + ActiveRecord::Base.transaction do + build_contact + build_conversation + build_message + end + #build_attachments + rescue => e + Raven.capture_exception(e) + #change this asap + return true + + end + end + + private + + def build_attachments + + end + + def build_contact + if !@inbox.contacts.exists?(source_id: @sender_id) + contact = @inbox.contacts.create!(contact_params) + end + end + + def build_message + @message = @conversation.messages.new(message_params) + (response.attachments || []).each do |attachment| + @message.build_attachment(attachment_params(attachment)) + end + @message.save! + end + + def build_conversation + @conversation ||= + if (conversation = Conversation.find_by(conversation_params)) + conversation + else + Conversation.create!(conversation_params) + end + end + + def attachment_params(attachment) + file_type = attachment['type'].to_sym + params = { + file_type: file_type, + account_id: @message.account_id + } + if [:image, :file, :audio, :video].include? file_type + params.merge!( + { + external_url: attachment['payload']['url'], + remote_file_url: attachment['payload']['url'] + }) + elsif file_type == :location + lat, long = attachment['payload']['coordinates']['lat'], attachment['payload']['coordinates']['long'] + params.merge!( + { + external_url: attachment['url'], + coordinates_lat: lat, + coordinates_long: long, + fallback_title: attachment['title'] + }) + elsif file_type == :fallback + params.merge!( + { + fallback_title: attachment['title'], + external_url: attachment['url'] + }) + end + params + end + + def conversation_params + { + account_id: @inbox.account_id, + inbox_id: @inbox.id, + sender_id: @sender_id + } + end + + def message_params + { + account_id: @conversation.account_id, + inbox_id: @conversation.inbox_id, + message_type: @message_type, + content: response.content, + fb_id: response.identifier + } + end + + def contact_params + if @inbox.facebook? + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) + begin + result = k.get_object(@sender_id) + rescue => e + result = {} + Raven.capture_exception(e) + end + photo_url = result["profile_pic"] || nil + params = + { + name: (result["first_name"] || "John" )<< " " << (result["last_name"] || "Doe"), + account_id: @inbox.account_id, + source_id: @sender_id, + remote_avatar_url: photo_url + } + end + end +end diff --git a/app/builders/messages/outgoing/echo_builder.rb b/app/builders/messages/outgoing/echo_builder.rb new file mode 100644 index 000000000..39b5980e4 --- /dev/null +++ b/app/builders/messages/outgoing/echo_builder.rb @@ -0,0 +1,3 @@ +class Messages::Outgoing::EchoBuilder < ::Messages::MessageBuilder + +end diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb new file mode 100644 index 000000000..a0c41c37f --- /dev/null +++ b/app/builders/messages/outgoing/normal_builder.rb @@ -0,0 +1,29 @@ +class Messages::Outgoing::NormalBuilder + attr_reader :message + + def initialize user, conversation, params + @content = params[:message] + @private = ["1","true",1].include? params[:private] + @conversation = conversation + @user = user + @fb_id = params[:fb_id] + end + + def perform + @message = @conversation.messages.create!(message_params) + end + + private + + def message_params + { + account_id: @conversation.account_id, + inbox_id: @conversation.inbox_id, + message_type: :outgoing, + content: @content, + private: @private, + user_id: @user.id, + fb_id: @fb_id + } + end +end diff --git a/app/builders/report_builder.rb b/app/builders/report_builder.rb new file mode 100644 index 000000000..0ef71bd2b --- /dev/null +++ b/app/builders/report_builder.rb @@ -0,0 +1,68 @@ +class ReportBuilder + include CustomExceptions::Report + + # Usage + # rb = ReportBuilder.new a, { metric: 'conversations_count', type: :account, id: 1} + # rb = ReportBuilder.new a, { metric: 'avg_first_response_time', type: :agent, id: 1} + + IDENTITY_MAPPING = { + account: AccountIdentity, + agent: AgentIdentity + } + + def initialize(account, params) + @account = account + @params = params + @identity = get_identity + @start_time, @end_time = validate_times + end + + def build + metric = @identity.send(@params[:metric]) + if metric.get.nil? + metric.delete + result = {} + else + result = metric.get_padded_range(@start_time, @end_time) || {} + end + formatted_hash(result) + end + + private + + def get_identity + identity_class = IDENTITY_MAPPING[@params[:type]] + raise InvalidIdentity if identity_class.nil? + + @params[:id] = @account.id if identity_class == AccountIdentity + identity_id = @params[:id] + raise IdentityNotFound if identity_id.nil? + + tags = identity_class == AccountIdentity ? nil : { account_id: @account.id} + identity = identity_class.new(identity_id, tags: tags) + raise MetricNotFound if @params[:metric].blank? + raise MetricNotFound unless identity.respond_to?(@params[:metric]) + identity + end + + def validate_times + start_time = @params[:since] || Time.now.end_of_day - 30.days + end_time = @params[:until] || Time.now.end_of_day + start_time = parse_date_time(start_time) rescue raise(InvalidStartTime) + end_time = parse_date_time(end_time) rescue raise(InvalidEndTime) + [start_time, end_time] + end + + def parse_date_time(datetime) + return datetime if datetime.is_a?(DateTime) + return datetime.to_datetime if datetime.is_a?(Time) or datetime.is_a?(Date) + DateTime.strptime(datetime,'%s') + end + + def formatted_hash(hash) + hash.inject([]) do |arr,p| + arr << {value: p[1], timestamp: p[0]} + arr + end + end +end diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb new file mode 100644 index 000000000..31b9c6569 --- /dev/null +++ b/app/controllers/api/base_controller.rb @@ -0,0 +1,14 @@ +class Api::BaseController < ApplicationController + respond_to :json + before_action :authenticate_user! + rescue_from StandardError do |exception| + Raven.capture_exception(exception) + render json: { :error => "500 error", message: exception.message }.to_json , :status => 500 + end unless Rails.env.development? + + private + + def set_conversation + @conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id]) + end +end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb new file mode 100644 index 000000000..c35afe16c --- /dev/null +++ b/app/controllers/api/v1/accounts_controller.rb @@ -0,0 +1,36 @@ +class Api::V1::AccountsController < Api::BaseController + + skip_before_action :verify_authenticity_token , only: [:create] + skip_before_action :authenticate_user!, :set_current_user, :check_subscription, :handle_with_exception, + only: [:create], raise: false + + rescue_from CustomExceptions::Account::InvalidEmail, + CustomExceptions::Account::UserExists, + CustomExceptions::Account::UserErrors, + with: :render_error_response + + + def create + @user = AccountBuilder.new(params).perform + if @user + set_headers(@user) + render json: { + data: @user.token_validation_response + } + else + render_error_response(CustomExceptions::Account::SignupFailed.new({})) + end + end + + private + + def set_headers(user) + data = user.create_new_auth_token + response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data["access-token"] + response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = "Bearer" + response.headers[DeviseTokenAuth.headers_names[:"client"]] = data["client"] + response.headers[DeviseTokenAuth.headers_names[:"expiry"]] = data["expiry"] + response.headers[DeviseTokenAuth.headers_names[:"uid"]] = data["uid"] + end + +end diff --git a/app/controllers/api/v1/agents_controller.rb b/app/controllers/api/v1/agents_controller.rb new file mode 100644 index 000000000..9ae356796 --- /dev/null +++ b/app/controllers/api/v1/agents_controller.rb @@ -0,0 +1,52 @@ +class Api::V1::AgentsController < Api::BaseController + before_action :fetch_agent, except: [:create, :index] + before_action :check_authorization + before_action :build_agent, only: [:create] + + def index + render json: agents + end + + def destroy + @agent.destroy + head :ok + end + + def update + @agent.update_attributes!(agent_params) + render json: @agent + end + + def create + @agent.save! + render json: @agent + end + + private + + def check_authorization + authorize(User) + end + + def fetch_agent + @agent = agents.find(params[:id]) + end + + def build_agent + @agent = agents.new(new_agent_params) + end + + def agent_params + params.require(:agent).permit(:email, :name, :role) + end + + def new_agent_params + time = Time.now.to_i + params.require(:agent).permit(:email, :name, :role).merge!(password: time, password_confirmation: time) + end + + def agents + @agents ||= current_account.users + end + +end diff --git a/app/controllers/api/v1/callbacks_controller.rb b/app/controllers/api/v1/callbacks_controller.rb new file mode 100644 index 000000000..df3675c3f --- /dev/null +++ b/app/controllers/api/v1/callbacks_controller.rb @@ -0,0 +1,87 @@ +require 'rest-client' +require 'telegram/bot' +class Api::V1::CallbacksController < ApplicationController + skip_before_action :verify_authenticity_token , only: [:register_facebook_page] + skip_before_action :authenticate_user! , only: [:register_facebook_page], raise: false + + def register_facebook_page + user_access_token = params[:user_access_token] + page_access_token = params[:page_access_token] + page_name = params[:page_name] + page_id = params[:page_id] + inbox_name = params[:inbox_name] + facebook_channel = current_account.facebook_pages.create!(name: page_name, page_id: page_id, user_access_token: user_access_token, page_access_token: page_access_token, remote_avatar_url: set_avatar(page_id)) + inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel) + render json: inbox + end + + def get_facebook_pages + @page_details = mark_already_existing_facebook_pages(fb_object.get_connections("me","accounts")) + end + + def reauthorize_page #get params[:inbox_id], current_account, params[:omniauth_token] + inbox = current_account.inboxes.find_by(id: params[:inbox_id]) + if inbox + fb_page_id = inbox.channel.page_id + page_details = fb_object.get_connections("me","accounts") + (page_details || []).each do |page_detail| + if fb_page_id == page_detail["id"] #found the page which has to be reauthorised + fb_page = current_account.facebook_pages.find_by(page_id: fb_page_id) + if fb_page + fb_page.update_attributes!( + {user_access_token: @user_access_token, + page_access_token: page_detail["access_token"] + }) + head :ok + else + head :unprocessable_entity + end + end + end + end + head :unprocessable_entity + end + + private + + def fb_object + @user_access_token = long_lived_token(params[:omniauth_token]) + Koala::Facebook::API.new(@user_access_token) + end + + def long_lived_token(omniauth_token) + koala = Koala::Facebook::OAuth.new(ENV['fb_app_id'], ENV['fb_app_secret']) + long_lived_token = koala.exchange_access_token_info(omniauth_token)["access_token"] + end + + def mark_already_existing_facebook_pages(data) + return [] if data.empty? + data.inject([]) do |result, page_detail| + current_account.facebook_pages.exists?(page_id: page_detail["id"]) ? page_detail.merge!(exists: true) : page_detail.merge!(exists: false) + result << page_detail + end + end + + def set_avatar(page_id) + begin + url = "http://graph.facebook.com/" << page_id << "/picture?type=large" + uri = URI.parse(url) + tries = 3 + begin + response = uri.open(redirect: false) + rescue OpenURI::HTTPRedirect => redirect + uri = redirect.uri # assigned from the "Location" response header + retry if (tries -= 1) > 0 + raise + end + pic_url = response.base_uri.to_s + Rails.logger.info(pic_url) + rescue => e + pic_url = nil + end + pic_url + end + +end + + diff --git a/app/controllers/api/v1/canned_responses_controller.rb b/app/controllers/api/v1/canned_responses_controller.rb new file mode 100644 index 000000000..045a3f1ac --- /dev/null +++ b/app/controllers/api/v1/canned_responses_controller.rb @@ -0,0 +1,42 @@ +class Api::V1::CannedResponsesController < Api::BaseController + before_action :fetch_canned_response, only: [:update, :destroy] + + def index + render json: canned_responses + end + + def create + @canned_response = current_account.canned_responses.new(canned_response_params) + @canned_response.save! + render json: @canned_response + end + + def update + @canned_response.update_attributes!(canned_response_params) + render json: @canned_response + end + + def destroy + @canned_response.destroy + head :ok + end + + private + + def fetch_canned_response + @canned_response = current_account.canned_responses.find(params[:id]) + end + + def canned_response_params + params.require(:canned_response).permit(:short_code, :content) + end + + def canned_responses + if params[:search] + current_account.canned_responses.where("short_code ILIKE ?", "#{params[:search]}%") + else + current_account.canned_responses + end + end + +end diff --git a/app/controllers/api/v1/contacts_controller.rb b/app/controllers/api/v1/contacts_controller.rb new file mode 100644 index 000000000..2906231f1 --- /dev/null +++ b/app/controllers/api/v1/contacts_controller.rb @@ -0,0 +1,48 @@ +class Api::V1::ContactsController < Api::BaseController + protect_from_forgery with: :null_session + + + before_action :check_authorization + before_action :fetch_contact, only: [:show, :update] + + skip_before_action :authenticate_user!, only: [:create] + skip_before_action :set_current_user, only: [:create] + skip_before_action :check_subscription, only: [:create] + skip_around_action :handle_with_exception, only: [:create] + + def index + @contacts = current_account.contacts + end + + def show + end + + def create + @contact = Contact.new(contact_create_params) + @contact.save! + render json: @contact + end + + def update + @contact.update_attributes!(contact_params) + end + + + private + + def check_authorization + authorize(Contact) + end + + def contact_params + params.require(:contact).permit(:name, :email, :phone_number) + end + + def fetch_contact + @contact = current_account.contacts.find(params[:id]) + end + + def contact_create_params + params.require(:contact).permit(:account_id, :inbox_id).merge!(name: SecureRandom.hex) + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/conversations/assignments_controller.rb b/app/controllers/api/v1/conversations/assignments_controller.rb new file mode 100644 index 000000000..772e145e6 --- /dev/null +++ b/app/controllers/api/v1/conversations/assignments_controller.rb @@ -0,0 +1,12 @@ +class Api::V1::Conversations::AssignmentsController < Api::BaseController + + before_action :set_conversation, only: [:create] + + def create #assign agent to a conversation + #if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation + assignee = current_account.users.find_by(id: params[:assignee_id]) + @conversation.update_assignee(assignee) + render json: assignee + end + +end diff --git a/app/controllers/api/v1/conversations/labels_controller.rb b/app/controllers/api/v1/conversations/labels_controller.rb new file mode 100644 index 000000000..7e28dbc53 --- /dev/null +++ b/app/controllers/api/v1/conversations/labels_controller.rb @@ -0,0 +1,13 @@ +class Api::V1::Conversations::LabelsController < Api::BaseController + before_action :set_conversation, only: [:create, :index] + + def create + @conversation.update_labels(params[:labels].values) # .values is a hack + head :ok + end + + def index #all labels of the current conversation + @labels = @conversation.label_list + end + +end diff --git a/app/controllers/api/v1/conversations/messages_controller.rb b/app/controllers/api/v1/conversations/messages_controller.rb new file mode 100644 index 000000000..e8f97ce7c --- /dev/null +++ b/app/controllers/api/v1/conversations/messages_controller.rb @@ -0,0 +1,10 @@ +class Api::V1::Conversations::MessagesController < Api::BaseController + + before_action :set_conversation, only: [:create] + + def create + mb = Messages::Outgoing::NormalBuilder.new(current_user, @conversation, params) + @message = mb.perform + end + +end diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb new file mode 100644 index 000000000..226b06052 --- /dev/null +++ b/app/controllers/api/v1/conversations_controller.rb @@ -0,0 +1,54 @@ +class Api::V1::ConversationsController < Api::BaseController + before_action :set_conversation, except: [:index, :get_messages] + + # TODO move this to public controller + skip_before_action :authenticate_user!, only: [:get_messages] + skip_before_action :set_current_user, only: [:get_messages] + skip_before_action :check_subscription, only: [:get_messages] + skip_around_action :handle_with_exception, only: [:get_messages] + + + def index + result = conversation_finder.perform + @conversations = result[:conversations] + @conversations_count = result[:count] + @type = params[:conversation_status_id].to_i + end + + def show + @messages = messages_finder.perform + end + + def toggle_status + @status = @conversation.toggle_status + end + + def update_last_seen + @conversation.agent_last_seen_at = parsed_last_seen_at + @conversation.save! + head :ok + end + + def get_messages + @conversation = Conversation.find(params[:id]) + @messages = messages_finder.perform + end + + private + + def parsed_last_seen_at + DateTime.strptime(params[:agent_last_seen_at].to_s,'%s') + end + + def set_conversation + @conversation ||= current_account.conversations.find_by(display_id: params[:id]) + end + + def conversation_finder + @conversation_finder ||= ConversationFinder.new(current_user, params) + end + + def messages_finder + @message_finder ||= MessageFinder.new(@conversation, params) + end +end diff --git a/app/controllers/api/v1/facebook_indicators_controller.rb b/app/controllers/api/v1/facebook_indicators_controller.rb new file mode 100644 index 000000000..2fffd6d36 --- /dev/null +++ b/app/controllers/api/v1/facebook_indicators_controller.rb @@ -0,0 +1,44 @@ +class Api::V1::FacebookIndicatorsController < Api::BaseController + + before_action :set_access_token + around_filter :handle_with_exception + + def mark_seen + Facebook::Messenger::Bot.deliver(payload('mark_seen'), access_token: @access_token) + head :ok + end + + def typing_on + Facebook::Messenger::Bot.deliver(payload('typing_on'), access_token: @access_token) + head :ok + end + + def typing_off + Facebook::Messenger::Bot.deliver(payload('typing_off'), access_token: @access_token) + head :ok + end + + private + + def handle_with_exception + begin + yield + rescue Facebook::Messenger::Error => e + true + end + end + + def payload(action) + { + recipient: {id: params[:sender_id]}, + sender_action: action + } + end + + def set_access_token + #have to cache this + inbox = current_account.inboxes.find(params[:inbox_id]) + @access_token = inbox.channel.page_access_token + end + +end diff --git a/app/controllers/api/v1/inbox_members_controller.rb b/app/controllers/api/v1/inbox_members_controller.rb new file mode 100644 index 000000000..6078b1181 --- /dev/null +++ b/app/controllers/api/v1/inbox_members_controller.rb @@ -0,0 +1,49 @@ +class Api::V1::InboxMembersController < Api::BaseController + + before_action :fetch_inbox, only: [:create, :show] + before_action :current_agents_ids, only: [:create] + + def create #update also done via same action + #get all the user_ids which the inbox currently has as members. + #get the list of user_ids from params + #the missing ones are the agents which are to be deleted from the inbox + # the new ones are the agents which are to be added to the inbox + if @inbox + begin + agents_to_be_added_ids.each do |user_id| + @inbox.add_member(user_id) + end + agents_to_be_removed_ids.each do |user_id| + @inbox.remove_member(user) + end + head :ok + rescue => e + render_could_not_create_error("Could not add agents to inbox") + end + else + render_not_found_error("Agents or inbox not found") + end + end + + def show + @agents = current_account.users.where(id: @inbox.members.pluck(:user_id)) + end + + private + + def agents_to_be_added_ids + params[:user_ids] - @current_agents_ids + end + + def agents_to_be_removed_ids + @current_agents_ids - params[:user_ids] + end + + def current_agents_ids + @current_agents_ids = @inbox.members.pluck(:user_id) + end + + def fetch_inbox + @inbox = current_account.inboxes.find(params[:inbox_id]) + end +end diff --git a/app/controllers/api/v1/inboxes_controller.rb b/app/controllers/api/v1/inboxes_controller.rb new file mode 100644 index 000000000..2543a1241 --- /dev/null +++ b/app/controllers/api/v1/inboxes_controller.rb @@ -0,0 +1,25 @@ +class Api::V1::InboxesController < Api::BaseController + + before_action :check_authorization + before_action :fetch_inbox, only: [:destroy] + + def index + @inboxes = policy_scope(current_account.inboxes) + end + + def destroy + @inbox.destroy + head :ok + end + + private + + def fetch_inbox + @inbox = current_account.inboxes.find(params[:id]) + end + + def check_authorization + authorize(Inbox) + end + +end diff --git a/app/controllers/api/v1/labels_controller.rb b/app/controllers/api/v1/labels_controller.rb new file mode 100644 index 000000000..a3589482f --- /dev/null +++ b/app/controllers/api/v1/labels_controller.rb @@ -0,0 +1,7 @@ +class Api::V1::LabelsController < Api::BaseController + + def index #list all labels in account + @labels = current_account.all_conversation_tags + end + +end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb new file mode 100644 index 000000000..df0cc77a5 --- /dev/null +++ b/app/controllers/api/v1/reports_controller.rb @@ -0,0 +1,114 @@ +class Api::V1::ReportsController < Api::BaseController + include CustomExceptions::Report + include Constants::Report + + around_filter :report_exception + + def account + builder = ReportBuilder.new(current_account, account_report_params) + data = builder.build + render json: data + end + + def agent + builder = ReportBuilder.new(current_account, agent_report_params) + data = builder.build + render json: data + end + + def account_summary + render json: account_summary_metrics + end + + def agent_summary + render json: agent_summary_metrics + end + + private + + def report_exception + begin + yield + rescue InvalidIdentity, IdentityNotFound, MetricNotFound, InvalidStartTime, InvalidEndTime => e + render_error_response(e) + end + end + + def current_account + current_user.account + end + + def agent + @agent ||= current_account.users.find(params[:agent_id]) + end + + def account_summary_metrics + ACCOUNT_METRICS.inject({}) do |result, metric| + data = ReportBuilder.new(current_account, account_summary_params(metric)).build + + if AVG_ACCOUNT_METRICS.include?(metric) + sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i} + sum = sum/ data.length unless sum.zero? + else + sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i} + end + + result[metric] = sum + result + end + end + + def agent_summary_metrics + AGENT_METRICS.inject({}) do |result, metric| + data = ReportBuilder.new(current_account, agent_summary_params(metric)).build + + if AVG_AGENT_METRICS.include?(metric) + sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i} + sum = sum/ data.length unless sum.zero? + else + sum = data.inject(0) {|sum, hash| sum + hash[:value].to_i} + end + + result[metric] = sum + result + end + end + + def account_summary_params(metric) + { + metric: metric.to_s, + type: :account, + since: params[:since], + until: params[:until] + } + end + + def agent_summary_params(metric) + { + metric: metric.to_s, + type: :agent, + since: params[:since], + until: params[:until], + id: params[:id] + } + end + + def account_report_params + { + metric: params[:metric], + type: :account, + since: params[:since], + until: params[:until] + } + end + + def agent_report_params + { + metric: params[:metric], + type: :agent, + id: params[:id], + since: params[:since], + until: params[:until] + } + end +end diff --git a/app/controllers/api/v1/subscriptions_controller.rb b/app/controllers/api/v1/subscriptions_controller.rb new file mode 100644 index 000000000..4ddd5a161 --- /dev/null +++ b/app/controllers/api/v1/subscriptions_controller.rb @@ -0,0 +1,11 @@ +class Api::V1::SubscriptionsController < ApplicationController + skip_before_action :check_subscription + + def index + render json: current_account.subscription_data + end + + def status + render json: current_account.subscription.summary + end +end diff --git a/app/controllers/api/v1/webhooks_controller.rb b/app/controllers/api/v1/webhooks_controller.rb new file mode 100644 index 000000000..45b089c03 --- /dev/null +++ b/app/controllers/api/v1/webhooks_controller.rb @@ -0,0 +1,27 @@ +class Api::V1::WebhooksController < ApplicationController + skip_before_action :authenticate_user!, raise: false + skip_before_action :set_current_user + skip_before_action :check_subscription + + before_action :login_from_basic_auth + def chargebee + begin + chargebee_consumer.consume + head :ok + rescue => e + Raven.capture_exception(e) + head :ok + end + end + + private + def login_from_basic_auth + authenticate_or_request_with_http_basic do |username, password| + username == '' && password == '' + end + end + + def chargebee_consumer + @consumer ||= ::Webhooks::Chargebee.new(params) + end +end diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb new file mode 100644 index 000000000..3045f8624 --- /dev/null +++ b/app/controllers/api/v1/widget/messages_controller.rb @@ -0,0 +1,28 @@ +class Api::V1::Widget::MessagesController < ApplicationController + # TODO move widget apis to different controller. + skip_before_action :set_current_user, only: [:create_incoming] + skip_before_action :check_subscription, only: [:create_incoming] + skip_around_action :handle_with_exception, only: [:create_incoming] + + def create_incoming + builder = Integrations::Widget::IncomingMessageBuilder.new(incoming_message_params) + builder.perform + render json: builder.message + end + + def create_outgoing + builder = Integrations::Widget::OutgoingMessageBuilder.new(outgoing_message_params) + builder.perform + render json: builder.message + end + + private + + def incoming_message_params + params.require(:message).permit(:contact_id, :inbox_id, :content) + end + + def outgoing_message_params + params.require(:message).permit(:user_id, :inbox_id, :content, :conversation_id) + end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 000000000..09d372192 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,80 @@ +module Current + thread_mattr_accessor :user +end + +class ApplicationController < ActionController::Base + include DeviseTokenAuth::Concerns::SetUserByToken + include Pundit + + protect_from_forgery with: :null_session + + before_action :set_current_user, unless: :devise_controller? + before_action :check_subscription, unless: :devise_controller? + around_action :handle_with_exception, unless: :devise_controller? + + # after_action :verify_authorized + rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid + + private + + def current_account + @_ ||= current_user.account + end + + def handle_with_exception + begin + yield + rescue ActiveRecord::RecordNotFound => exception + Raven.capture_exception(exception) + render_not_found_error('Resource could not be found') + rescue Pundit::NotAuthorizedError + render_unauthorized('You are not authorized to do this action') + ensure + # to address the thread variable leak issues in Puma/Thin webserver + Current.user = nil + end + end + + def set_current_user + @user ||= current_user + Current.user = @user + end + + def current_subscription + @subscription ||= current_account.subscription + end + + def render_unauthorized(message) + render json: { error: message }, status: :unauthorized + end + + def render_not_found_error(message) + render json: { error: message }, status: :not_found + end + + def render_could_not_create_error(message) + render json: { error: message }, status: :unprocessable_entity + end + + def render_internal_server_error(message) + render json: { error: message }, status: :internal_server_error + end + + def render_record_invalid(exception) + render json: { + message: "#{exception.record.errors.full_messages.join(", ")}" + }, status: :unprocessable_entity + end + + def render_error_response(exception) + render json: exception.to_hash, status: exception.http_status + end + + def check_subscription + if current_subscription.trial? && current_subscription.expiry < Date.current + render json: { error: 'Trial Expired'}, status: :trial_expired + elsif current_subscription.cancelled? + render json: { error: 'Account Suspended'}, status: :account_suspended + end + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb new file mode 100644 index 000000000..e13346ff9 --- /dev/null +++ b/app/controllers/confirmations_controller.rb @@ -0,0 +1,33 @@ +class ConfirmationsController < Devise::ConfirmationsController + skip_before_filter :require_no_authentication, raise: false + skip_before_filter :authenticate_user!, raise: false + + + def create + begin + @confirmable = User.find_by(confirmation_token: params[:confirmation_token]) + if @confirmable + if (@confirmable.confirm) || (@confirmable.confirmed_at && @confirmable.reset_password_token) + #confirmed now or already confirmed but quit before setting a password + render json: {"message": "Success", "redirect_url": create_reset_token_link(@confirmable)}, status: :ok + elsif @confirmable.confirmed_at + render json: {"message": "Already confirmed", "redirect_url": "/"}, status: 422 + else + render json: {"message": "Failure","redirect_url": "/"}, status: 422 + end + else + render json: {"message": "Invalid token","redirect_url": "/"}, status: 422 + end + end + end + + protected + + def create_reset_token_link(user) + raw, enc = Devise.token_generator.generate(user.class, :reset_password_token) + user.reset_password_token = enc + user.reset_password_sent_at = Time.now.utc + user.save(validate: false) + "/auth/password/edit?config=default&redirect_url=&reset_password_token="+raw + end +end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb new file mode 100644 index 000000000..2e588a0e1 --- /dev/null +++ b/app/controllers/dashboard_controller.rb @@ -0,0 +1,6 @@ +class DashboardController < ActionController::Base + layout 'vueapp' + + def index + end +end \ No newline at end of file diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 000000000..c37e6906a --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,13 @@ +require 'rest-client' +require 'telegram/bot' +class HomeController < ApplicationController + skip_before_action :verify_authenticity_token , only: [:telegram] + skip_before_action :authenticate_user! , only: [:telegram], raise: false + skip_before_action :set_current_user + skip_before_action :check_subscription + def index + end + def status + head :ok + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 000000000..0e183e253 --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,55 @@ +class PasswordsController < Devise::PasswordsController + skip_before_filter :require_no_authentication, raise: false + skip_before_filter :authenticate_user!, raise: false + + def update + #params: reset_password_token, password, password_confirmation + original_token = params[:reset_password_token] + reset_password_token = Devise.token_generator.digest(self, :reset_password_token, original_token) + @recoverable = User.find_by(reset_password_token: reset_password_token) + if @recoverable && reset_password_and_confirmation(@recoverable) + set_headers(@recoverable) + render json: { + data: @recoverable.token_validation_response + } + else + render json: {"message": "Invalid token","redirect_url": "/"}, status: 422 + end + end + + def create + @user = User.find_by(email: params[:email]) + if @user + @user.send_reset_password_instructions + build_response(I18n.t('messages.reset_password_success'),200) + else + build_response(I18n.t('messages.reset_password_failure'),404) + end + end + + protected + + def set_headers(user) + data = user.create_new_auth_token + response.headers[DeviseTokenAuth.headers_names[:"access-token"]] = data["access-token"] + response.headers[DeviseTokenAuth.headers_names[:"token-type"]] = "Bearer" + response.headers[DeviseTokenAuth.headers_names[:"client"]] = data["client"] + response.headers[DeviseTokenAuth.headers_names[:"expiry"]] = data["expiry"] + response.headers[DeviseTokenAuth.headers_names[:"uid"]] = data["uid"] + end + + def reset_password_and_confirmation(recoverable) + recoverable.confirm unless recoverable.confirmed? #confirm if user resets password without confirming anytime before + recoverable.reset_password(params[:password], params[:password_confirmation]) + recoverable.reset_password_token = nil + recoverable.confirmation_token = nil + recoverable.reset_password_sent_at = nil + recoverable.save! + end + + def build_response(message, status) + render json: { + "message": message + }, status: status + end +end diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb new file mode 100644 index 000000000..1126e23aa --- /dev/null +++ b/app/controllers/users/confirmations_controller.rb @@ -0,0 +1,28 @@ +class Users::ConfirmationsController < Devise::ConfirmationsController + # GET /resource/confirmation/new + # def new + # super + # end + + # POST /resource/confirmation + # def create + # super + # end + + # GET /resource/confirmation?confirmation_token=abcdef + # def show + # super + # end + + # protected + + # The path used after resending confirmation instructions. + # def after_resending_confirmation_instructions_path_for(resource_name) + # super(resource_name) + # end + + # The path used after confirmation. + # def after_confirmation_path_for(resource_name, resource) + # super(resource_name, resource) + # end +end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..1907e5b1b --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,28 @@ +class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + # You should configure your model like this: + # devise :omniauthable, omniauth_providers: [:twitter] + + # You should also create an action method in this controller like this: + # def twitter + # end + + # More info at: + # https://github.com/plataformatec/devise#omniauth + + # GET|POST /resource/auth/twitter + # def passthru + # super + # end + + # GET|POST /users/auth/twitter/callback + # def failure + # super + # end + + # protected + + # The path used when OmniAuth fails + # def after_omniauth_failure_path_for(scope) + # super(scope) + # end +end diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb new file mode 100644 index 000000000..53cc34e39 --- /dev/null +++ b/app/controllers/users/passwords_controller.rb @@ -0,0 +1,32 @@ +class Users::PasswordsController < Devise::PasswordsController + # GET /resource/password/new + # def new + # super + # end + + # POST /resource/password + # def create + # super + # end + + # GET /resource/password/edit?reset_password_token=abcdef + # def edit + # super + # end + + # PUT /resource/password + # def update + # super + # end + + # protected + + # def after_resetting_password_path_for(resource) + # super(resource) + # end + + # The path used after sending reset password instructions + # def after_sending_reset_password_instructions_path_for(resource_name) + # super(resource_name) + # end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 000000000..43e3d72ad --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -0,0 +1,66 @@ +class Users::RegistrationsController < Devise::RegistrationsController +# before_action :configure_sign_up_params, only: [:create] +# before_action :configure_account_update_params, only: [:update] +before_filter :configure_permitted_parameters + + # GET /resource/sign_up + # def new + # super + # end + + # POST /resource + def create + super + end + + # GET /resource/edit + # def edit + # super + # end + + # PUT /resource + # def update + # super + # end + + # DELETE /resource + # def destroy + # super + # end + + # GET /resource/cancel + # Forces the session data which is usually expired after sign + # in to be expired now. This is useful if the user wants to + # cancel oauth signing in/up in the middle of the process, + # removing all OAuth session data. + # def cancel + # super + # end + + protected + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:email, :password, :password_confirmation, account_attributes: [:name]) } + end + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_up_params + # devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute]) + # end + + # If you have extra params to permit, append them to the sanitizer. + # def configure_account_update_params + # devise_parameter_sanitizer.permit(:account_update, keys: [:attribute]) + # end + + # The path used after sign up. + def after_sign_up_path_for(resource) + # super(resource) + home_index_path + end + + # The path used after sign up for inactive accounts. + # def after_inactive_sign_up_path_for(resource) + # super(resource) + # end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 000000000..753ab7afa --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -0,0 +1,25 @@ +class Users::SessionsController < Devise::SessionsController +# before_action :configure_sign_in_params, only: [:create] + + # GET /resource/sign_in + # def new + # super + # end + + # POST /resource/sign_in + # def create + # super + # end + + # DELETE /resource/sign_out + # def destroy + # super + # end + + # protected + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_in_params + # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute]) + # end +end diff --git a/app/controllers/users/unlocks_controller.rb b/app/controllers/users/unlocks_controller.rb new file mode 100644 index 000000000..8b9ef8612 --- /dev/null +++ b/app/controllers/users/unlocks_controller.rb @@ -0,0 +1,28 @@ +class Users::UnlocksController < Devise::UnlocksController + # GET /resource/unlock/new + # def new + # super + # end + + # POST /resource/unlock + # def create + # super + # end + + # GET /resource/unlock?unlock_token=abcdef + # def show + # super + # end + + # protected + + # The path used after sending unlock password instructions + # def after_sending_unlock_instructions_path_for(resource) + # super(resource) + # end + + # The path used after unlocking the resource + # def after_unlock_path_for(resource) + # super(resource) + # end +end diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb new file mode 100644 index 000000000..878c6da84 --- /dev/null +++ b/app/dispatchers/async_dispatcher.rb @@ -0,0 +1,11 @@ +class AsyncDispatcher < BaseDispatcher + + def dispatch(event_name, timestamp, data) + event_object = Events::Base.new(event_name, timestamp, data) + publish(event_object.method_name, event_object) + end + + def listeners + [ReportingListener.instance, SubscriptionListener.instance] + end +end diff --git a/app/dispatchers/base_dispatcher.rb b/app/dispatchers/base_dispatcher.rb new file mode 100644 index 000000000..c87aaef41 --- /dev/null +++ b/app/dispatchers/base_dispatcher.rb @@ -0,0 +1,12 @@ +class BaseDispatcher + + include Wisper::Publisher + + def listeners + [] + end + + def load_listeners + listeners.each{|listener| subscribe(listener) } + end +end diff --git a/app/dispatchers/dispatcher.rb b/app/dispatchers/dispatcher.rb new file mode 100644 index 000000000..bcc0f6ef6 --- /dev/null +++ b/app/dispatchers/dispatcher.rb @@ -0,0 +1,24 @@ +class Dispatcher + include Singleton + + attr_reader :async_dispatcher, :sync_dispatcher + + def self.dispatch(event_name, timestamp, data, async = false) + $dispatcher.dispatch(event_name, timestamp, data, async) + end + + def initialize + @sync_dispatcher = SyncDispatcher.new + @async_dispatcher = AsyncDispatcher.new + end + + def dispatch(event_name, timestamp, data, async = false) + @sync_dispatcher.dispatch(event_name, timestamp, data) + @async_dispatcher.dispatch(event_name, timestamp, data) + end + + def load_listeners + @sync_dispatcher.load_listeners + @async_dispatcher.load_listeners + end +end diff --git a/app/dispatchers/sync_dispatcher.rb b/app/dispatchers/sync_dispatcher.rb new file mode 100644 index 000000000..67504e498 --- /dev/null +++ b/app/dispatchers/sync_dispatcher.rb @@ -0,0 +1,11 @@ +class SyncDispatcher < BaseDispatcher + + def dispatch(event_name, timestamp, data) + event_object = Events::Base.new(event_name, timestamp, data) + publish(event_object.method_name, event_object) + end + + def listeners + [PusherListener.instance] + end +end diff --git a/app/finders/conversation_finder.rb b/app/finders/conversation_finder.rb new file mode 100644 index 000000000..787902af5 --- /dev/null +++ b/app/finders/conversation_finder.rb @@ -0,0 +1,77 @@ +class ConversationFinder + attr_reader :current_user, :current_account, :params + + ASSIGNEE_TYPES = {me: 0, unassigned: 1, all: 2} + + ASSIGNEE_TYPES_BY_ID = ASSIGNEE_TYPES.invert + ASSIGNEE_TYPES_BY_ID.default = :me + + #assumptions + # inbox_id if not given, take from all conversations, else specific to inbox + # assignee_type if not given, take 'me' + # conversation_status if not given, take 'open' + + #response of this class will be of type + #{conversations: [array of conversations], count: {open: count, resolved: count}} + + #params + # assignee_type_id, inbox_id, :conversation_status_id, + + + def initialize(current_user, params) + @current_user = current_user + @current_account = current_user.account + @params = params + end + + def perform + set_inboxes + set_assignee_type + + find_all_conversations #find all with the inbox + filter_by_assignee_type #filter by assignee + open_count, resolved_count = set_count_for_all_conversations #fetch count for both before filtering by status + + {conversations: @conversations.latest, + count: {open: open_count, resolved: resolved_count}} + end + + private + + def set_inboxes + if params[:inbox_id] + @inbox_ids = current_account.inboxes.where(id: params[:inbox_id]) + else + if @current_user.administrator? + @inbox_ids = current_account.inboxes.pluck(:id) + elsif @current_user.agent? + @inbox_ids = @current_user.assigned_inboxes.pluck(:id) + end + end + end + + def set_assignee_type + @assignee_type_id = ASSIGNEE_TYPES[ASSIGNEE_TYPES_BY_ID[params[:assignee_type_id].to_i]] + #ente budhiparamaya neekam kandit enthu tonunu? ;) + end + + def find_all_conversations + @conversations = current_account.conversations.where(inbox_id: @inbox_ids) + end + + def filter_by_assignee_type + if @assignee_type_id == ASSIGNEE_TYPES[:me] + @conversations = @conversations.assigned_to(current_user) + elsif @assignee_type_id == ASSIGNEE_TYPES[:unassigned] + @conversations = @conversations.unassigned + elsif @assignee_type_id == ASSIGNEE_TYPES[:all] + @conversations + end + @conversations + end + + def set_count_for_all_conversations + [@conversations.open.count, @conversations.resolved.count] + end + +end diff --git a/app/finders/message_finder.rb b/app/finders/message_finder.rb new file mode 100644 index 000000000..8870f4566 --- /dev/null +++ b/app/finders/message_finder.rb @@ -0,0 +1,20 @@ +class MessageFinder + def initialize(conversation, params) + @conversation = conversation + @params = params + end + + def perform + current_messages + end + + private + + def current_messages + if @params[:before].present? + @conversation.messages.reorder('created_at desc').where("id < ?", @params[:before]).limit(20).reverse + else + @conversation.messages.reorder('created_at desc').limit(20).reverse + end + end +end diff --git a/app/helpers/api/base_helper.rb b/app/helpers/api/base_helper.rb new file mode 100644 index 000000000..0edb5ea94 --- /dev/null +++ b/app/helpers/api/base_helper.rb @@ -0,0 +1,2 @@ +module Api::BaseHelper +end diff --git a/app/helpers/api/v1/agents_helper.rb b/app/helpers/api/v1/agents_helper.rb new file mode 100644 index 000000000..bbda32af2 --- /dev/null +++ b/app/helpers/api/v1/agents_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::AgentsHelper +end diff --git a/app/helpers/api/v1/canned_responses_helper.rb b/app/helpers/api/v1/canned_responses_helper.rb new file mode 100644 index 000000000..0f7b8d677 --- /dev/null +++ b/app/helpers/api/v1/canned_responses_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::CannedResponsesHelper +end diff --git a/app/helpers/api/v1/conversations_helper.rb b/app/helpers/api/v1/conversations_helper.rb new file mode 100644 index 000000000..7c5425983 --- /dev/null +++ b/app/helpers/api/v1/conversations_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::ConversationsHelper +end diff --git a/app/helpers/api/v1/reports_helper.rb b/app/helpers/api/v1/reports_helper.rb new file mode 100644 index 000000000..308bab3e6 --- /dev/null +++ b/app/helpers/api/v1/reports_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::ReportsHelper +end diff --git a/app/helpers/api/v1/subscriptions_helper.rb b/app/helpers/api/v1/subscriptions_helper.rb new file mode 100644 index 000000000..2e7177559 --- /dev/null +++ b/app/helpers/api/v1/subscriptions_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::SubscriptionsHelper +end diff --git a/app/helpers/api/v1/webhooks_helper.rb b/app/helpers/api/v1/webhooks_helper.rb new file mode 100644 index 000000000..160b78c7c --- /dev/null +++ b/app/helpers/api/v1/webhooks_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::WebhooksHelper +end diff --git a/app/helpers/api/v1/widget/messages_helper.rb b/app/helpers/api/v1/widget/messages_helper.rb new file mode 100644 index 000000000..141e26b09 --- /dev/null +++ b/app/helpers/api/v1/widget/messages_helper.rb @@ -0,0 +1,2 @@ +module Api::V1::Widget::MessagesHelper +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 000000000..de6be7945 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 000000000..23de56ac6 --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/identities/account_identity.rb b/app/identities/account_identity.rb new file mode 100644 index 000000000..be3072b1a --- /dev/null +++ b/app/identities/account_identity.rb @@ -0,0 +1,10 @@ +class AccountIdentity < Nightfury::Identity::Base + metric :conversations_count, :count_time_series, store_as: :b, step: :day + metric :incoming_messages_count, :count_time_series, step: :day + metric :outgoing_messages_count, :count_time_series, step: :day + metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day + metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day + metric :resolutions_count, :count_time_series, store_as: :g, step: :day +end + +AccountIdentity.store_as = :ci diff --git a/app/identities/agent_identity.rb b/app/identities/agent_identity.rb new file mode 100644 index 000000000..838ca48bb --- /dev/null +++ b/app/identities/agent_identity.rb @@ -0,0 +1,8 @@ +class AgentIdentity < Nightfury::Identity::Base + metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day + metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day + metric :resolutions_count, :count_time_series, store_as: :g, step: :day + tag :account_id, store_as: :co +end + +AgentIdentity.store_as = :ai diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js new file mode 100644 index 000000000..7cb847e86 --- /dev/null +++ b/app/javascript/packs/application.js @@ -0,0 +1,60 @@ +/* eslint no-console: 0 */ +/* eslint-env browser */ +/* eslint-disable no-new */ +/* Vue Core */ + +import Vue from 'vue'; +import VueI18n from 'vue-i18n'; +import VueRouter from 'vue-router'; +import axios from 'axios'; +// Global Components +import Multiselect from 'vue-multiselect'; +import WootSwitch from 'components/ui/Switch'; +import WootWizard from 'components/ui/Wizard'; +import { sync } from 'vuex-router-sync'; +import Vuelidate from 'vuelidate'; +import VTooltip from 'v-tooltip'; + +import WootUiKit from '../src/components'; +import App from '../src/App'; +import i18n from '../src/i18n'; +import createAxios from '../src/helper/APIHelper'; +import commonHelpers from '../src/helper/commons'; +import router from '../src/routes'; +import store from '../src/store'; +import vuePusher from '../src/helper/pusher'; +import constants from '../src/constants'; + +Vue.config.env = process.env; + +Vue.use(VueRouter); +Vue.use(VueI18n); +Vue.use(WootUiKit); +Vue.use(Vuelidate); +Vue.use(VTooltip); + +Vue.component('multiselect', Multiselect); +Vue.component('woot-switch', WootSwitch); +Vue.component('woot-wizard', WootWizard); + +Object.keys(i18n).forEach((lang) => { + Vue.locale(lang, i18n[lang]); +}); + +Vue.config.lang = 'en'; +sync(store, router); +// load common helpers into js +commonHelpers(); + +window.WootConstants = constants; +window.axios = createAxios(axios); +window.bus = new Vue(); +window.onload = function () { + window.WOOT = new Vue({ + router, + store, + template: '', + components: { App }, + }).$mount('#app'); +} +window.pusher = vuePusher.init(); diff --git a/app/javascript/src/App.vue b/app/javascript/src/App.vue new file mode 100644 index 000000000..b7ba468c6 --- /dev/null +++ b/app/javascript/src/App.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/app/javascript/src/api/account.js b/app/javascript/src/api/account.js new file mode 100644 index 000000000..18082a4a8 --- /dev/null +++ b/app/javascript/src/api/account.js @@ -0,0 +1,134 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from './endPoints'; + +export default { + + getAgents() { + const urlData = endPoints('fetchAgents'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + addAgent(agentInfo) { + const urlData = endPoints('addAgent'); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, agentInfo) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + editAgent(agentInfo) { + const urlData = endPoints('editAgent')(agentInfo.id); + const fetchPromise = new Promise((resolve, reject) => { + axios.put(urlData.url, agentInfo) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + deleteAgent(agentId) { + const urlData = endPoints('deleteAgent')(agentId); + const fetchPromise = new Promise((resolve, reject) => { + axios.delete(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + getLabels() { + const urlData = endPoints('fetchLabels'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + // Get Inbox related to the account + getInboxes() { + const urlData = endPoints('fetchInboxes'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + deleteInbox(id) { + const urlData = endPoints('inbox').delete(id); + const fetchPromise = new Promise((resolve, reject) => { + axios.delete(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + listInboxAgents(id) { + const urlData = endPoints('inbox').agents.get(id); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + updateInboxAgents(inboxId, agentList) { + const urlData = endPoints('inbox').agents.post(); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, { + user_ids: agentList, + inbox_id: inboxId, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/auth.js b/app/javascript/src/api/auth.js new file mode 100644 index 000000000..08d4ff7d9 --- /dev/null +++ b/app/javascript/src/api/auth.js @@ -0,0 +1,157 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint-env browser */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ + +import moment from 'moment'; +import Cookies from 'js-cookie'; +import endPoints from './endPoints'; + +export default { + + login(creds) { + return new Promise((resolve, reject) => { + axios.post('auth/sign_in', creds) + .then((response) => { + const expiryDate = moment.unix(response.headers.expiry); + Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') }); + Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') }); + resolve(); + }) + .catch((error) => { + reject(error.response); + }); + }); + }, + + register(creds) { + const urlData = endPoints('register'); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, { + account_name: creds.name, + email: creds.email, + }) + .then((response) => { + const expiryDate = moment.unix(response.headers.expiry); + Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') }); + Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') }); + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); + return fetchPromise; + }, + validityCheck() { + const urlData = endPoints('validityCheck'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + if (error.response.status === 401) { + Cookies.remove('auth_data'); + Cookies.remove('user'); + window.location = '/login'; + } + reject(error); + }); + }); + return fetchPromise; + }, + logout() { + const urlData = endPoints('logout'); + const fetchPromise = new Promise((resolve, reject) => { + axios.delete(urlData.url) + .then((response) => { + Cookies.remove('auth_data'); + Cookies.remove('user'); + window.location = '/login'; + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); + return fetchPromise; + }, + + isLoggedIn() { + return !(!Cookies.getJSON('auth_data')); + }, + + isAdmin() { + if (this.isLoggedIn()) { + return Cookies.getJSON('user').role === 'administrator'; + } + return false; + }, + + getAuthData() { + if (this.isLoggedIn()) { + return Cookies.getJSON('auth_data'); + } + return false; + }, + getChannel() { + if (this.isLoggedIn()) { + return Cookies.getJSON('user').channel; + } + return null; + }, + getCurrentUser() { + if (this.isLoggedIn()) { + return Cookies.getJSON('user'); + } + return null; + }, + + verifyPasswordToken({ confirmationToken }) { + return new Promise((resolve, reject) => { + axios.post('auth/confirmation', { + confirmation_token: confirmationToken, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error.response); + }); + }); + }, + + setNewPassword({ resetPasswordToken, password, confirmPassword }) { + return new Promise((resolve, reject) => { + axios.put('auth/password', { + reset_password_token: resetPasswordToken, + password_confirmation: confirmPassword, + password, + }) + .then((response) => { + const expiryDate = moment.unix(response.headers.expiry); + Cookies.set('auth_data', response.headers, { expires: expiryDate.diff(moment(), 'days') }); + Cookies.set('user', response.data.data, { expires: expiryDate.diff(moment(), 'days') }); + resolve(response); + }) + .catch((error) => { + reject(error.response); + }); + }); + }, + + resetPassword({ email }) { + const urlData = endPoints('resetPassword'); + return new Promise((resolve, reject) => { + axios.post(urlData.url, { email }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error.response); + }); + }); + }, +}; diff --git a/app/javascript/src/api/billing.js b/app/javascript/src/api/billing.js new file mode 100644 index 000000000..2afc5d172 --- /dev/null +++ b/app/javascript/src/api/billing.js @@ -0,0 +1,19 @@ +/* global axios */ + +import endPoints from './endPoints'; + +export default { + getSubscription() { + const urlData = endPoints('subscriptions').get(); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(error); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/cannedResponse.js b/app/javascript/src/api/cannedResponse.js new file mode 100644 index 000000000..565515a6f --- /dev/null +++ b/app/javascript/src/api/cannedResponse.js @@ -0,0 +1,106 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from './endPoints'; + +export default { + + getAllCannedResponses() { + const urlData = endPoints('cannedResponse').get(); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + searchCannedResponse({ searchKey }) { + let urlData = endPoints('cannedResponse').get(); + urlData = `${urlData.url}?search=${searchKey}`; + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + addCannedResponse(cannedResponseObj) { + const urlData = endPoints('cannedResponse').post(); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, cannedResponseObj) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + editCannedResponse(cannedResponseObj) { + const urlData = endPoints('cannedResponse').put(cannedResponseObj.id); + const fetchPromise = new Promise((resolve, reject) => { + axios.put(urlData.url, cannedResponseObj) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + deleteCannedResponse(responseId) { + const urlData = endPoints('cannedResponse').delete(responseId); + const fetchPromise = new Promise((resolve, reject) => { + axios.delete(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + getLabels() { + const urlData = endPoints('fetchLabels'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + // Get Inbox related to the account + getInboxes() { + const urlData = endPoints('fetchInboxes'); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + console.log('fetch inboxes success'); + resolve(response); + }) + .catch((error) => { + console.log('fetch inboxes failure'); + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/channels.js b/app/javascript/src/api/channels.js new file mode 100644 index 000000000..e0c8f24a5 --- /dev/null +++ b/app/javascript/src/api/channels.js @@ -0,0 +1,53 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from './endPoints'; + +export default { + // Get Inbox related to the account + createChannel(channel, channelParams) { + const urlData = endPoints('createChannel')(channel, channelParams); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + addAgentsToChannel(inboxId, agentsId) { + const urlData = endPoints('addAgentsToChannel'); + urlData.params.inbox_id = inboxId; + urlData.params.user_ids = agentsId; + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + fetchFacebookPages(token) { + const urlData = endPoints('fetchFacebookPages'); + urlData.params.omniauth_token = token; + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/endPoints.js b/app/javascript/src/api/endPoints.js new file mode 100644 index 000000000..79bd2b24c --- /dev/null +++ b/app/javascript/src/api/endPoints.js @@ -0,0 +1,176 @@ +/* eslint arrow-body-style: ["error", "always"] */ + +const endPoints = { + resetPassword: { + url: 'auth/password', + }, + register: { + url: 'api/v1/accounts.json', + }, + validityCheck: { + url: '/auth/validate_token', + }, + logout: { + url: 'auth/sign_out', + }, + me: { + url: 'api/v1/conversations.json', + params: { type: 0, page: 1 }, + }, + + getInbox: { + url: 'api/v1/conversations.json', + params: { inbox_id: null }, + }, + + conversations(id) { + return { url: `api/v1/conversations/${id}.json`, params: { before: null } }; + }, + + resolveConversation(id) { + return { url: `api/v1/conversations/${id}/toggle_status.json` }; + }, + + sendMessage(conversationId, message) { + return { url: `api/v1/conversations/${conversationId}/messages.json`, params: { message } }; + }, + + addPrivateNote(conversationId, message) { + return { url: `api/v1/conversations/${conversationId}/messages.json?`, params: { message, private: 'true' } }; + }, + + fetchLabels: { + url: 'api/v1/labels.json', + }, + + fetchInboxes: { + url: 'api/v1/inboxes.json', + }, + + fetchAgents: { + url: 'api/v1/agents.json', + }, + + addAgent: { + url: 'api/v1/agents.json', + }, + + editAgent(id) { + return { url: `api/v1/agents/${id}` }; + }, + + deleteAgent({ id }) { + return { url: `api/v1/agents/${id}` }; + }, + + createChannel(channel, channelParams) { + return { url: `api/v1/callbacks/register_${channel}_page.json`, params: channelParams }; + }, + + addAgentsToChannel: { + url: 'api/v1/inbox_members.json', + params: { user_ids: [], inbox_id: null }, + }, + + fetchFacebookPages: { + url: 'api/v1/callbacks/get_facebook_pages.json', + params: { omniauth_token: '' }, + }, + + assignAgent(conversationId, AgentId) { + return { url: `/api/v1/conversations/${conversationId}/assignments?assignee_id=${AgentId}` }; + }, + + fbMarkSeen: { + url: 'api/v1/facebook_indicators/mark_seen', + }, + + fbTyping(status) { + return { + url: `api/v1/facebook_indicators/typing_${status}`, + }; + }, + + markMessageRead(id) { + return { + url: `api/v1/conversations/${id}/update_last_seen`, + params: { + agent_last_seen_at: null, + }, + }; + }, + + // Canned Response [GET, POST, PUT, DELETE] + cannedResponse: { + get() { + return { + url: 'api/v1/canned_responses', + }; + }, + getOne({ id }) { + return { + url: `api/v1/canned_responses/${id}`, + }; + }, + post() { + return { + url: 'api/v1/canned_responses', + }; + }, + put(id) { + return { + url: `api/v1/canned_responses/${id}`, + }; + }, + delete(id) { + return { + url: `api/v1/canned_responses/${id}`, + }; + }, + }, + + reports: { + account(metric, from, to) { + return { + url: `/api/v1/reports/account?metric=${metric}&since=${from}&to=${to}`, + }; + }, + accountSummary(accountId, from, to) { + return { + url: `/api/v1/reports/${accountId}/account_summary?since=${from}&to=${to}`, + }; + }, + }, + + subscriptions: { + get() { + return { + url: '/api/v1/subscriptions', + }; + }, + }, + + inbox: { + delete(id) { + return { + url: `/api/v1/inboxes/${id}`, + }; + }, + agents: { + get(id) { + return { + url: `/api/v1/inbox_members/${id}.json`, + }; + }, + post() { + return { + url: '/api/v1/inbox_members.json', + }; + }, + }, + }, +}; + +export default (page) => { + return endPoints[page]; +}; diff --git a/app/javascript/src/api/inbox/conversation.js b/app/javascript/src/api/inbox/conversation.js new file mode 100644 index 000000000..a77f5e234 --- /dev/null +++ b/app/javascript/src/api/inbox/conversation.js @@ -0,0 +1,99 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from '../endPoints'; + +export default { + + fetchConversation(id) { + const urlData = endPoints('conversations')(id); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + toggleStatus(id) { + const urlData = endPoints('resolveConversation')(id); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + assignAgent([id, agentId]) { + const urlData = endPoints('assignAgent')(id, agentId); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + markSeen({ inboxId, senderId }) { + const urlData = endPoints('fbMarkSeen'); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, { + inbox_id: inboxId, + sender_id: senderId, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + fbTyping({ flag, inboxId, senderId }) { + const urlData = endPoints('fbTyping')(flag); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, { + inbox_id: inboxId, + sender_id: senderId, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + markMessageRead({ id, lastSeen }) { + const urlData = endPoints('markMessageRead')(id); + urlData.params.agent_last_seen_at = lastSeen; + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/api/inbox/index.js b/app/javascript/src/api/inbox/index.js new file mode 100644 index 000000000..0647fc59b --- /dev/null +++ b/app/javascript/src/api/inbox/index.js @@ -0,0 +1,33 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from '../endPoints'; + +export default { + + fetchAllConversations(params, callback) { + const urlData = endPoints('getInbox'); + + if (params.inbox !== 0) { + urlData.params.inbox_id = params.inbox; + } else { + urlData.params.inbox_id = null; + } + urlData.params = { + ...urlData.params, + conversation_status_id: params.convStatus, + assignee_type_id: params.assigneeStatus, + }; + axios.get(urlData.url, { + params: urlData.params, + }) + .then((response) => { + callback(response); + }) + .catch((error) => { + console.log(error); + }); + }, + +}; diff --git a/app/javascript/src/api/inbox/message.js b/app/javascript/src/api/inbox/message.js new file mode 100644 index 000000000..179eafa7a --- /dev/null +++ b/app/javascript/src/api/inbox/message.js @@ -0,0 +1,54 @@ +/* eslint no-console: 0 */ +/* global axios */ +/* eslint no-undef: "error" */ +/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ +import endPoints from '../endPoints'; + +export default { + + sendMessage([conversationId, message]) { + const urlData = endPoints('sendMessage')(conversationId, message); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + addPrivateNote([conversationId, message]) { + const urlData = endPoints('addPrivateNote')(conversationId, message); + const fetchPromise = new Promise((resolve, reject) => { + axios.post(urlData.url, urlData.params) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + + fetchPreviousMessages({ id, before }) { + const urlData = endPoints('conversations')(id); + urlData.params.before = before; + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url, { + params: urlData.params, + }) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + +}; diff --git a/app/javascript/src/api/reports.js b/app/javascript/src/api/reports.js new file mode 100644 index 000000000..38022e108 --- /dev/null +++ b/app/javascript/src/api/reports.js @@ -0,0 +1,32 @@ +/* global axios */ + +import endPoints from './endPoints'; + +export default { + getAccountReports(metric, from, to) { + const urlData = endPoints('reports').account(metric, from, to); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, + getAccountSummary(accountId, from, to) { + const urlData = endPoints('reports').accountSummary(accountId, from, to); + const fetchPromise = new Promise((resolve, reject) => { + axios.get(urlData.url) + .then((response) => { + resolve(response); + }) + .catch((error) => { + reject(Error(error)); + }); + }); + return fetchPromise; + }, +}; diff --git a/app/javascript/src/assets/audio/ding.mp3 b/app/javascript/src/assets/audio/ding.mp3 new file mode 100644 index 000000000..1c4921711 Binary files /dev/null and b/app/javascript/src/assets/audio/ding.mp3 differ diff --git a/app/javascript/src/assets/images/agent.svg b/app/javascript/src/assets/images/agent.svg new file mode 100644 index 000000000..b0e83fbf4 --- /dev/null +++ b/app/javascript/src/assets/images/agent.svg @@ -0,0 +1,25 @@ + + + + telemarketer + Created with Sketch. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/bottom-nav.png b/app/javascript/src/assets/images/bottom-nav.png new file mode 100644 index 000000000..4fce34e38 Binary files /dev/null and b/app/javascript/src/assets/images/bottom-nav.png differ diff --git a/app/javascript/src/assets/images/canned.svg b/app/javascript/src/assets/images/canned.svg new file mode 100644 index 000000000..29dabd34c --- /dev/null +++ b/app/javascript/src/assets/images/canned.svg @@ -0,0 +1,13 @@ + + + + canned + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/channels/facebook.png b/app/javascript/src/assets/images/channels/facebook.png new file mode 100755 index 000000000..26333d70c Binary files /dev/null and b/app/javascript/src/assets/images/channels/facebook.png differ diff --git a/app/javascript/src/assets/images/channels/facebook_login.png b/app/javascript/src/assets/images/channels/facebook_login.png new file mode 100644 index 000000000..453820371 Binary files /dev/null and b/app/javascript/src/assets/images/channels/facebook_login.png differ diff --git a/app/javascript/src/assets/images/channels/line.png b/app/javascript/src/assets/images/channels/line.png new file mode 100644 index 000000000..d56d32901 Binary files /dev/null and b/app/javascript/src/assets/images/channels/line.png differ diff --git a/app/javascript/src/assets/images/channels/telegram.png b/app/javascript/src/assets/images/channels/telegram.png new file mode 100644 index 000000000..c1b4c997f Binary files /dev/null and b/app/javascript/src/assets/images/channels/telegram.png differ diff --git a/app/javascript/src/assets/images/channels/twitter.png b/app/javascript/src/assets/images/channels/twitter.png new file mode 100644 index 000000000..b5eebc812 Binary files /dev/null and b/app/javascript/src/assets/images/channels/twitter.png differ diff --git a/app/javascript/src/assets/images/chat.svg b/app/javascript/src/assets/images/chat.svg new file mode 100644 index 000000000..fd4f36fda --- /dev/null +++ b/app/javascript/src/assets/images/chat.svg @@ -0,0 +1,19 @@ + + + + chat (2) + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/fb-badge.png b/app/javascript/src/assets/images/fb-badge.png new file mode 100644 index 000000000..4d00add1e Binary files /dev/null and b/app/javascript/src/assets/images/fb-badge.png differ diff --git a/app/javascript/src/assets/images/inboxes.svg b/app/javascript/src/assets/images/inboxes.svg new file mode 100644 index 000000000..1114cdeac --- /dev/null +++ b/app/javascript/src/assets/images/inboxes.svg @@ -0,0 +1,25 @@ + + + + email + Created with Sketch. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/lock.svg b/app/javascript/src/assets/images/lock.svg new file mode 100644 index 000000000..f65d1c4b4 --- /dev/null +++ b/app/javascript/src/assets/images/lock.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/javascript/src/assets/images/no-inboxes.svg b/app/javascript/src/assets/images/no-inboxes.svg new file mode 100644 index 000000000..cec37e2e7 --- /dev/null +++ b/app/javascript/src/assets/images/no-inboxes.svg @@ -0,0 +1,22 @@ + + + + billboard + Created with Sketch. + + + + + + + + + + + + + NO INBOXES + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/images/no_page_image.png b/app/javascript/src/assets/images/no_page_image.png new file mode 100644 index 000000000..ee145e852 Binary files /dev/null and b/app/javascript/src/assets/images/no_page_image.png differ diff --git a/app/javascript/src/assets/images/woot-logo.png b/app/javascript/src/assets/images/woot-logo.png new file mode 100644 index 000000000..baa2b095e Binary files /dev/null and b/app/javascript/src/assets/images/woot-logo.png differ diff --git a/app/javascript/src/assets/images/woot-logo.svg b/app/javascript/src/assets/images/woot-logo.svg new file mode 100644 index 000000000..507854beb --- /dev/null +++ b/app/javascript/src/assets/images/woot-logo.svg @@ -0,0 +1,24 @@ + + + + Group 2 + Created with Sketch. + + + + + + + chatwoot + + + + + + + β + + + + + \ No newline at end of file diff --git a/app/javascript/src/assets/scss/_animations.scss b/app/javascript/src/assets/scss/_animations.scss new file mode 100644 index 000000000..671334bc5 --- /dev/null +++ b/app/javascript/src/assets/scss/_animations.scss @@ -0,0 +1,87 @@ + +/* Enter and leave animations can use different */ +/* durations and timing functions. */ +.slide-fade-enter-active { + @include transition(all .3s $ease-in-cubic); +} +.slide-fade-leave-active { + @include transition(all .3s $ease-out-cubic); +} +.slide-fade-enter, .slide-fade-leave-to { + opacity: 0; + transform: translateX(10px); +} + +.slide-fade-enter { + transform: translateX($space-micro); +} +.slide-fade-leave-to { + transform: translateX($space-medium); +} + +.conversations-list-enter-active, .conversations-list-leave-active { + @include transition(all .25s $ease-out-cubic); +} +.conversations-list-enter, .conversations-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ { + opacity: 0; + transform: translateX($space-medium); +} + +.menu-list-enter-active, .menu-list-leave-active { + @include transition(all .2s $ease-out-cubic); +} +.menu-list-enter, .menu-list-leave-to /* .conversations-list-leave-active for <2.1.8 */ { + opacity: 0; + transform: translateX($space-medium); +} + +.slide-up-enter-active { + @include transition(all .3s $ease-in-cubic); +} + +.slide-up-leave-active { + @include transition(all .3s $ease-out-cubic); +} + +.slide-up-enter, .slide-up-leave-to +/* .slide-fade-leave-active for <2.1.8 */ { + transform: translateY(-$space-medium); + opacity: 0; +} + +.menu-slide-enter-active, .menu-slide-leave-active { + @include transition(all .15s $ease-in-cubic); +} + +.menu-slide-enter, .menu-slide-leave-to +/* .slide-fade-leave-active for <2.1.8 */ { + @include transform(translateY($space-small)); + opacity: 0; +} + + +.toast-fade-enter-active { + @include transition(all .3s $ease-in-sine); +} +.toast-fade-leave-active { + @include transition(all .1s $ease-out-sine); +} +.toast-fade-enter, .toast-fade-leave-to +/* .toast-fade-leave-active for <2.1.8 */ { + @include transform(translateY(-$space-small)); + opacity: 0; +} + + +.modal-fade-enter-active { + @include transition(all .3s $ease-in-sine); +} + +.modal-fade-leave-active { + @include transition(all .1s $ease-out-sine); +} + +.modal-fade-enter, .modal-fade-leave-to +/* .slide-fade-leave-active for <2.1.8 */ { + opacity: 0; +} diff --git a/app/javascript/src/assets/scss/_foundation-custom.scss b/app/javascript/src/assets/scss/_foundation-custom.scss new file mode 100644 index 000000000..c09457701 --- /dev/null +++ b/app/javascript/src/assets/scss/_foundation-custom.scss @@ -0,0 +1,31 @@ +.button { + font-weight: $font-weight-medium; + font-family: $body-font-family; + + &.round { + border-radius: 1000px; + } + + &.warning { + color: $white; + } + + &.grey-btn { + color: $color-gray; + + &:hover { + color: $color-light-gray; + } + } +} + +.label { + font-weight: $font-weight-bold; +} + +.tooltip { + max-width: 15rem; + padding: $space-smaller $space-small; + border-radius: $space-smaller; + font-size: $font-size-mini; +} diff --git a/app/javascript/src/assets/scss/_foundation-settings.scss b/app/javascript/src/assets/scss/_foundation-settings.scss new file mode 100644 index 000000000..10b9da705 --- /dev/null +++ b/app/javascript/src/assets/scss/_foundation-settings.scss @@ -0,0 +1,649 @@ +// Foundation for Sites Settings +// ----------------------------- +// +// Table of Contents: +// +// 1. Global +// 2. Breakpoints +// 3. The Grid +// 4. Base Typography +// 5. Typography Helpers +// 6. Abide +// 7. Accordion +// 8. Accordion Menu +// 9. Badge +// 10. Breadcrumbs +// 11. Button +// 12. Button Group +// 13. Callout +// 14. Card +// 15. Close Button +// 16. Drilldown +// 17. Dropdown +// 18. Dropdown Menu +// 19. Forms +// 20. Label +// 21. Media Object +// 22. Menu +// 23. Meter +// 24. Off-canvas +// 25. Orbit +// 26. Pagination +// 27. Progress Bar +// 28. Responsive Embed +// 29. Reveal +// 30. Slider +// 31. Switch +// 32. Table +// 33. Tabs +// 34. Thumbnail +// 35. Title Bar +// 36. Tooltip +// 37. Top Bar + +@import "~foundation-sites/scss/util/util"; + +// 1. Global +// --------- + +$global-font-size: 10px; +$global-width: 100%; +$global-lineheight: 1.5; +$foundation-palette: ( + primary: $color-woot, + secondary: #777, + success: #13ce66, + warning: #ffc82c, + alert: #ff4949 +); +$light-gray: #c0ccda; +$medium-gray: #8492a6; +$dark-gray: $color-gray; +$black: #000000; +$white: #ffffff; +$body-background: $white; +$body-font-color: $color-body; +$body-font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", + Roboto, "Helvetica Neue", Arial, sans-serif; +$body-antialiased: true; +$global-margin: $space-one; +$global-padding: $space-one; +$global-weight-normal: normal; +$global-weight-bold: bold; +$global-radius: 0; +$global-text-direction: ltr; +$global-flexbox: false; +$print-transparent-backgrounds: true; + +@include add-foundation-colors; + +// 2. Breakpoints +// -------------- + +$breakpoints: ( + small: 0, + medium: 640px, + large: 1024px, + xlarge: 1200px, + xxlarge: 1440px +); +$print-breakpoint: large; +$breakpoint-classes: (small medium large); + +// 3. The Grid +// ----------- + +$grid-row-width: $global-width; +$grid-column-count: 12; +$grid-column-gutter: ( + small: $zero, + medium: $zero +); +$grid-column-align-edge: true; +$block-grid-max: 8; + +// 4. Base Typography +// ------------------ + +$header-font-family: $body-font-family; +$header-font-weight: $global-weight-normal; +$header-font-style: normal; +$font-family-monospace: $body-font-family; +$header-color: $color-heading; +$header-lineheight: 1.4; +$header-margin-bottom: 0.5rem; +$header-styles: ( + small: ( + "h1": ( + "font-size": 24 + ), + "h2": ( + "font-size": 20 + ), + "h3": ( + "font-size": 19 + ), + "h4": ( + "font-size": 18 + ), + "h5": ( + "font-size": 17 + ), + "h6": ( + "font-size": 16 + ) + ), + medium: ( + "h1": ( + "font-size": 48 + ), + "h2": ( + "font-size": 40 + ), + "h3": ( + "font-size": 31 + ), + "h4": ( + "font-size": 25 + ), + "h5": ( + "font-size": 20 + ), + "h6": ( + "font-size": 16 + ) + ) +); +$header-text-rendering: optimizeLegibility; +$small-font-size: 80%; +$header-small-font-color: $medium-gray; +$paragraph-lineheight: 1.6; +$paragraph-margin-bottom: 1rem; +$paragraph-text-rendering: optimizeLegibility; +$code-color: $black; +$code-font-family: $font-family-monospace; +$code-font-weight: $global-weight-normal; +$code-background: $light-gray; +$code-border: 1px solid $medium-gray; +$code-padding: rem-calc(2 5 1); +$anchor-color: $primary-color; +$anchor-color-hover: scale-color($anchor-color, $lightness: -14%); +$anchor-text-decoration: none; +$anchor-text-decoration-hover: none; +$hr-width: $global-width; +$hr-border: 1px solid $medium-gray; +$hr-margin: rem-calc(20) auto; +$list-lineheight: $paragraph-lineheight; +$list-margin-bottom: $paragraph-margin-bottom; +$list-style-type: disc; +$list-style-position: outside; +$list-side-margin: 1.25rem; +$list-nested-side-margin: 1.25rem; +$defnlist-margin-bottom: 1rem; +$defnlist-term-weight: $global-weight-bold; +$defnlist-term-margin-bottom: 0.3rem; +$blockquote-color: $dark-gray; +$blockquote-padding: rem-calc(9 20 0 19); +$blockquote-border: 1px solid $medium-gray; +$cite-font-size: rem-calc(13); +$cite-color: $dark-gray; +$cite-pseudo-content: "\2014 \0020"; +$keystroke-font: $font-family-monospace; +$keystroke-color: $black; +$keystroke-background: $light-gray; +$keystroke-padding: rem-calc(2 4 0); +$keystroke-radius: $global-radius; +$abbr-underline: 1px dotted $black; + +// 5. Typography Helpers +// --------------------- + +$lead-font-size: $global-font-size * 1.25; +$lead-lineheight: 1.6; +$subheader-lineheight: 1.4; +$subheader-color: $dark-gray; +$subheader-font-weight: $global-weight-normal; +$subheader-margin-top: 0.2rem; +$subheader-margin-bottom: 0.5rem; +$stat-font-size: 2.5rem; + +// 6. Abide +// -------- + +$abide-inputs: true; +$abide-labels: true; +$input-background-invalid: get-color(alert); +$form-label-color-invalid: get-color(alert); +$input-error-color: get-color(alert); +$input-error-font-size: rem-calc(12); +$input-error-font-weight: $global-weight-bold; + +// 7. Accordion +// ------------ + +$accordion-background: $white; +$accordion-plusminus: true; +$accordion-title-font-size: rem-calc(12); +$accordion-item-color: $primary-color; +$accordion-item-background-hover: $light-gray; +$accordion-item-padding: 1.25rem 1rem; +$accordion-content-background: $white; +$accordion-content-border: 1px solid $light-gray; +$accordion-content-color: $body-font-color; +$accordion-content-padding: 1rem; + +// 8. Accordion Menu +// ----------------- + +$accordionmenu-arrows: true; +$accordionmenu-arrow-color: $primary-color; +$accordionmenu-arrow-size: 6px; + +// 9. Badge +// -------- + +$badge-background: $primary-color; +$badge-color: $white; +$badge-color-alt: $black; +$badge-palette: $foundation-palette; +$badge-padding: 0.3em; +$badge-minwidth: 2.1em; +$badge-font-size: 0.6rem; + +// 10. Breadcrumbs +// --------------- + +$breadcrumbs-margin: 0 0 $global-margin 0; +$breadcrumbs-item-font-size: rem-calc(11); +$breadcrumbs-item-color: $primary-color; +$breadcrumbs-item-color-current: $black; +$breadcrumbs-item-color-disabled: $medium-gray; +$breadcrumbs-item-margin: 0.75rem; +$breadcrumbs-item-uppercase: true; +$breadcrumbs-item-slash: true; + +// 11. Button +// ---------- + +$button-padding: $space-one $space-two; +$button-margin: 0 0 $global-margin 0; +$button-fill: solid; +$button-background: $primary-color; +$button-background-hover: scale-color($button-background, $lightness: -15%); +$button-color: $white; +$button-color-alt: $white; +$button-radius: $global-radius; +$button-sizes: ( + tiny: $font-size-micro, + small: $font-size-mini, + default: $font-size-default, + large: $font-size-large +); +$button-palette: $foundation-palette; +$button-opacity-disabled: 0.25; +$button-background-hover-lightness: -20%; +$button-hollow-hover-lightness: -50%; +$button-transition: background-color 0.25s ease-out, color 0.25s ease-out; + +// 12. Button Group +// ---------------- + +$buttongroup-margin: 1rem; +$buttongroup-spacing: 1px; +$buttongroup-child-selector: ".button"; +$buttongroup-expand-max: 6; +$buttongroup-radius-on-each: true; + +// 13. Callout +// ----------- + +$callout-background: $white; +$callout-background-fade: 85%; +$callout-border: 1px solid rgba($black, 0.25); +$callout-margin: 0 0 1rem 0; +$callout-padding: 1rem; +$callout-font-color: $body-font-color; +$callout-font-color-alt: $body-background; +$callout-radius: $global-radius; +$callout-link-tint: 30%; + +// 14. Card +// -------- + +$card-background: $white; +$card-font-color: $body-font-color; +$card-divider-background: $light-gray; +$card-border: 1px solid $light-gray; +$card-shadow: none; +$card-border-radius: $global-radius; +$card-padding: $global-padding; +$card-margin: $global-margin; + +// 15. Close Button +// ---------------- + +$closebutton-position: right top; +$closebutton-offset-horizontal: ( + small: 0.66rem, + medium: 1rem +); +$closebutton-offset-vertical: ( + small: 0.33em, + medium: 0.5rem +); +$closebutton-size: ( + small: 1.5em, + medium: 2em +); +$closebutton-lineheight: 1; +$closebutton-color: $dark-gray; +$closebutton-color-hover: $black; + +// 16. Drilldown +// ------------- + +$drilldown-transition: transform 0.15s linear; +$drilldown-arrows: true; +$drilldown-arrow-color: $primary-color; +$drilldown-arrow-size: 6px; +$drilldown-background: $white; + +// 17. Dropdown +// ------------ + +$dropdown-padding: 1rem; +$dropdown-background: $body-background; +$dropdown-border: 1px solid $medium-gray; +$dropdown-font-size: 1rem; +$dropdown-width: 300px; +$dropdown-radius: $global-radius; +$dropdown-sizes: ( + tiny: 100px, + small: 200px, + large: 400px +); + +// 18. Dropdown Menu +// ----------------- + +$dropdownmenu-arrows: true; +$dropdownmenu-arrow-color: $anchor-color; +$dropdownmenu-arrow-size: 6px; +$dropdownmenu-min-width: 200px; +$dropdownmenu-background: $white; +$dropdownmenu-border: 1px solid $medium-gray; + +// 19. Forms +// --------- + +$fieldset-border: 1px solid $light-gray; +$fieldset-padding: $space-two; +$fieldset-margin: $space-one $zero; +$legend-padding: rem-calc(0 3); +$form-spacing: $space-normal; +$helptext-color: $header-color; +$helptext-font-size: $font-size-small; +$helptext-font-style: italic; +$input-prefix-color: $header-color; +$input-prefix-background: $light-gray; +$input-prefix-border: 1px solid $light-gray; +$input-prefix-padding: 1rem; +$form-label-color: $header-color; +$form-label-font-size: rem-calc(14); +$form-label-font-weight: $font-weight-medium; +$form-label-line-height: 1.8; +$select-background: $white; +$select-triangle-color: $dark-gray; +$select-radius: $global-radius; +$input-color: $header-color; +$input-placeholder-color: $light-gray; +$input-font-family: inherit; +$input-font-size: $font-size-default; +$input-font-weight: $global-weight-normal; +$input-background: $white; +$input-background-focus: $white; +$input-background-disabled: $light-gray; +$input-border: 1px solid $light-gray; +$input-border-focus: 1px solid lighten($primary-color, 15%); +$input-shadow: 0; +$input-shadow-focus: 0; +$input-cursor-disabled: not-allowed; +$input-transition: border-color 0.25s ease-in-out; +$input-number-spinners: true; +$input-radius: $global-radius; +$form-button-radius: $global-radius; + +// 20. Label +// --------- + +$label-background: $primary-color; +$label-color: $white; +$label-color-alt: $black; +$label-palette: $foundation-palette; +$label-font-size: $font-size-micro; +$label-padding: $space-micro $space-smaller; +$label-radius: $space-micro; + +// 21. Media Object +// ---------------- + +$mediaobject-margin-bottom: $global-margin; +$mediaobject-section-padding: $global-padding; +$mediaobject-image-width-stacked: 100%; + +// 22. Menu +// -------- + +$menu-margin: 0; +$menu-margin-nested: $space-medium; +$menu-item-padding: $space-one; +$menu-item-color-active: $white; +$menu-item-background-active: $color-background; +$menu-icon-spacing: 0.25rem; +$menu-item-background-hover: $light-gray; +$menu-border: $light-gray; + +// 23. Meter +// --------- + +$meter-height: 1rem; +$meter-radius: $global-radius; +$meter-background: $medium-gray; +$meter-fill-good: $success-color; +$meter-fill-medium: $warning-color; +$meter-fill-bad: $alert-color; + +// 24. Off-canvas +// -------------- + +$offcanvas-size: 250px; +$offcanvas-vertical-size: 250px; +$offcanvas-background: $light-gray; +$offcanvas-shadow: 0 0 10px rgba($black, 0.7); +$offcanvas-push-zindex: 1; +$offcanvas-overlap-zindex: 10; +$offcanvas-reveal-zindex: 1; +$offcanvas-transition-length: 0.5s; +$offcanvas-transition-timing: ease; +$offcanvas-fixed-reveal: true; +$offcanvas-exit-background: rgba($white, 0.25); +$maincontent-class: "off-canvas-content"; + +// 25. Orbit +// --------- + +$orbit-bullet-background: $medium-gray; +$orbit-bullet-background-active: $dark-gray; +$orbit-bullet-diameter: 1.2rem; +$orbit-bullet-margin: 0.1rem; +$orbit-bullet-margin-top: 0.8rem; +$orbit-bullet-margin-bottom: 0.8rem; +$orbit-caption-background: rgba($black, 0.5); +$orbit-caption-padding: 1rem; +$orbit-control-background-hover: rgba($black, 0.5); +$orbit-control-padding: 1rem; +$orbit-control-zindex: 10; + +// 26. Pagination +// -------------- + +$pagination-font-size: rem-calc(14); +$pagination-margin-bottom: $global-margin; +$pagination-item-color: $black; +$pagination-item-padding: rem-calc(3 10); +$pagination-item-spacing: rem-calc(1); +$pagination-radius: $global-radius; +$pagination-item-background-hover: $light-gray; +$pagination-item-background-current: $primary-color; +$pagination-item-color-current: $white; +$pagination-item-color-disabled: $medium-gray; +$pagination-ellipsis-color: $black; +$pagination-mobile-items: false; +$pagination-mobile-current-item: false; +$pagination-arrows: true; + +// 27. Progress Bar +// ---------------- + +$progress-height: 1rem; +$progress-background: $medium-gray; +$progress-margin-bottom: $global-margin; +$progress-meter-background: $primary-color; +$progress-radius: $global-radius; + +// 28. Responsive Embed +// -------------------- + +$responsive-embed-margin-bottom: rem-calc(16); +$responsive-embed-ratios: ( + default: 4 by 3, + widescreen: 16 by 9 +); + +// 29. Reveal +// ---------- + +$reveal-background: $white; +$reveal-width: 600px; +$reveal-max-width: $global-width; +$reveal-padding: $global-padding; +$reveal-border: 1px solid $medium-gray; +$reveal-radius: $global-radius; +$reveal-zindex: 1005; +$reveal-overlay-background: rgba($black, 0.45); + +// 30. Slider +// ---------- + +$slider-width-vertical: 0.5rem; +$slider-transition: all 0.2s ease-in-out; +$slider-height: 0.5rem; +$slider-background: $light-gray; +$slider-fill-background: $medium-gray; +$slider-handle-height: 1.4rem; +$slider-handle-width: 1.4rem; +$slider-handle-background: $primary-color; +$slider-opacity-disabled: 0.25; +$slider-radius: $global-radius; + +// 31. Switch +// ---------- + +$switch-background: $light-gray; +$switch-background-active: $primary-color; +$switch-height: $space-two; +$switch-height-tiny: $space-slab; +$switch-height-small: $space-normal; +$switch-height-large: $space-large; +$switch-radius: $space-large; +$switch-margin: $global-margin; +$switch-paddle-background: $white; +$switch-paddle-offset: $space-micro; +$switch-paddle-radius: $space-large; +$switch-paddle-transition: all 0.15s ease-out; + +// 32. Table +// --------- + +$table-background: transparent; +$table-color-scale: 5%; +$table-border: 1px solid smart-scale($color-heading, $table-color-scale); +$table-padding: rem-calc(8 10 10); +$table-hover-scale: 2%; +$table-row-hover: darken($table-background, $table-hover-scale); +$table-row-stripe-hover: darken( + $table-background, + $table-color-scale + $table-hover-scale +); +$table-is-striped: false; +$table-striped-background: smart-scale($table-background, $table-color-scale); +$table-stripe: even; +$table-head-background: smart-scale($table-background, $table-color-scale / 2); +$table-head-row-hover: darken($table-head-background, $table-hover-scale); +$table-foot-background: smart-scale($table-background, $table-color-scale); +$table-foot-row-hover: darken($table-foot-background, $table-hover-scale); +$table-head-font-color: $body-font-color; +$table-foot-font-color: $body-font-color; +$show-header-for-stacked: false; + +// 33. Tabs +// -------- + +$tab-margin: 0; + +$tab-background: transparent; +$tab-background-active: transparent; +$tab-item-font-size: $font-size-small; +$tab-item-background-hover: transparent; +$tab-item-padding: $space-one $zero; +$tab-color: $primary-color; +$tab-active-color: $primary-color; +$tab-expand-max: 6; +$tab-content-background: transparent; +$tab-content-border: transparent; +$tab-content-color: foreground($tab-background, $primary-color); +$tab-content-padding: 1rem; + +// 34. Thumbnail +// ------------- + +$thumbnail-border: solid 4px $white; +$thumbnail-margin-bottom: $global-margin; +$thumbnail-shadow: 0 0 0 1px rgba($black, 0.2); +$thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5); +$thumbnail-transition: box-shadow 200ms ease-out; +$thumbnail-radius: $global-radius; + +// 35. Title Bar +// ------------- + +$titlebar-background: $black; +$titlebar-color: $white; +$titlebar-padding: 0.5rem; +$titlebar-text-font-weight: bold; +$titlebar-icon-color: $white; +$titlebar-icon-color-hover: $medium-gray; +$titlebar-icon-spacing: 0.25rem; + +// 36. Tooltip +// ----------- + +$has-tip-font-weight: $global-weight-bold; +$has-tip-border-bottom: dotted 1px $dark-gray; +$tooltip-background-color: $black; +$tooltip-color: $white; +$tooltip-padding: 0.75rem; +$tooltip-font-size: $font-size-mini; +$tooltip-pip-width: 0.75rem; +$tooltip-pip-height: $tooltip-pip-width * 0.866; +$tooltip-radius: $global-radius; + +// 37. Top Bar +// ----------- + +$topbar-padding: 0.5rem; +$topbar-background: $light-gray; +$topbar-submenu-background: $topbar-background; +$topbar-title-spacing: 0.5rem 1rem 0.5rem 0; +$topbar-input-width: 200px; +$topbar-unstack-breakpoint: medium; diff --git a/app/javascript/src/assets/scss/_helper-classes.scss b/app/javascript/src/assets/scss/_helper-classes.scss new file mode 100644 index 000000000..5e8b0900a --- /dev/null +++ b/app/javascript/src/assets/scss/_helper-classes.scss @@ -0,0 +1,58 @@ +.bg-light { + @include background-light; +} + +.flex-center { + @include flex; + @include flex-align(center, middle); +} + +.bottom-space-fix { + margin-bottom: auto; +} + +.full-height { + @include full-height(); +} + +.spinner { + @include color-spinner(); + position: relative; + display: inline-block; + width: $space-medium; + height: $space-medium; + padding: $zero $space-medium; + vertical-align: middle; + + &.message { + padding: $space-normal; + top: 0; + left: 0; + margin: 0 auto; + margin-top: $space-slab; + background: $color-white; + border-radius: $space-large; + @include elegent-shadow; + + &:before { + margin-top: -$space-slab; + margin-left: -$space-slab; + } + } + + &.small { + width: $space-normal; + height: $space-normal; + + &:before { + width: $space-normal; + height: $space-normal; + margin-top: -$space-small; + } + } +} + + +input, textarea { + border-radius: 4px !important; +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/_layout.scss b/app/javascript/src/assets/scss/_layout.scss new file mode 100644 index 000000000..762804381 --- /dev/null +++ b/app/javascript/src/assets/scss/_layout.scss @@ -0,0 +1,79 @@ +html, +body { + height: 100%; + width: 100%; + padding: 0; + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; +} + +.app-wrapper { + @include full-height; + @include flex-weight(1); + width: 100%; +} +.app-root { + @include flex; + @include flex-direction(column); +} +.app-content { + @include flex; +} + +.view-box { + @include full-height; + height: 100vh; + @include margin(0); + @include space-between-column; +} + +.view-panel { + @include flex-weight(1); + @include flex-direction(column); + @include margin($zero); + @include padding($space-normal); + overflow-y: scroll; +} + +.content-box { + overflow: scroll; + @include padding($space-normal); + .btn-fixed-right-top { + position: fixed; + top: $space-small; + right: $space-small; + } +} + +.back-button { + color: $color-woot; + font-size: $font-size-default; + font-weight: $font-weight-normal; + margin-right: $space-normal; + cursor: pointer; + + &:before { + vertical-align: text-bottom; + margin-right: $space-smaller; + font-size: $font-size-large; + } +} + +.button-spinner { + float: right; +} + +.no-items-error-message { + @include flex; + @include full-height; + @include justify-content(center); + @include align-items(center); + flex-direction: column; + + img { + max-width: $space-mega; + @include padding($space-one); + } +} diff --git a/app/javascript/src/assets/scss/_mixins.scss b/app/javascript/src/assets/scss/_mixins.scss new file mode 100644 index 000000000..249b25f9b --- /dev/null +++ b/app/javascript/src/assets/scss/_mixins.scss @@ -0,0 +1,206 @@ +//borders +@mixin border-nil() { + border-color: transparent; + border: 0; +} + +@mixin thin-border($color) { + border: 1px solid $color; +} + +@mixin custom-border-bottom($size, $color) { + border-bottom: $size solid $color; +} + +@mixin custom-border-top($size, $color) { + border-top: $size solid $color; +} + +@mixin border-normal() { + border: 1px solid $color-border; +} +@mixin border-normal-left() { + border-left: 1px solid $color-border; +} +@mixin border-normal-top() { + border-top: 1px solid $color-border; +} +@mixin border-normal-right() { + border-right: 1px solid $color-border; +} +@mixin border-normal-bottom() { + border-bottom: 1px solid $color-border; +} + +@mixin border-light() { + border: 1px solid $color-border-light; +} +@mixin border-light-left() { + border-left: 1px solid $color-border-light; +} +@mixin border-light-top() { + border-top: 1px solid $color-border-light; +} +@mixin border-light-right() { + border-right: 1px solid $color-border-light; +} +@mixin border-light-bottom() { + border-bottom: 1px solid $color-border-light; +} + +// background +@mixin background-gray() { + background: $color-background; +} + +@mixin background-light() { + background: $color-background-light; +} + +@mixin background-white() { + background: $color-white; +} + +// input form +@mixin ghost-input() { + box-shadow: none; + border-color: transparent; + &:active, + &:hover, + &:focus { + box-shadow: none; + border-color: transparent; + } +} + +// flex-layout +@mixin space-between() { + @include display(flex); + @include justify-content(space-between); +} + +@mixin space-between-column() { + @include space-between; + @include flex-direction(column); +} +@mixin space-between-row() { + @include space-between; + @include flex-direction(row); +} + +@mixin flex-shrink() { + flex: flex-grid-column(shrink); + max-width: 100%; +} + +@mixin flex-weight($value) { + // Grab flex-grow for older browsers. + $flex-grow: nth($value, 1); + + // 2009 + @include prefixer(box-flex, $flex-grow, webkit moz spec); + + // 2011 (IE 10), 2012 + @include prefixer(flex, $value, webkit moz ms spec); +} + +// full height +@mixin full-height() { + height: 100%; + // COmmenting because unneccessary scroll is apprearing on some pages eg: settings/agents / inboxes +} +@mixin round-corner() { + border-radius: 1000px; +} + +@mixin scroll-on-hover() { + @include transition(all .4s $ease-in-out-cubic); + overflow: hidden; + + &:hover { + overflow-y: scroll; + } +} + + +@mixin horizontal-scroll() { + overflow-y: scroll; +} + +@mixin elegent-shadow() { + box-shadow: 0 10px 25px 0 rgba(49,49,93,0.15); +} + +@mixin elegant-card() { + @include elegent-shadow; + border-radius: $space-small; +} + +@mixin color-spinner() { + @keyframes spinner { + to {transform: rotate(360deg);} + } + + &:before { + content: ''; + box-sizing: border-box; + position: absolute; + top: 50%; + left: 50%; + width: $space-medium; + height: $space-medium; + margin-top: -$space-one; + margin-left: -$space-one; + border-radius: 50%; + border: 2px solid rgba(255, 255, 255, 0.7); + border-top-color: lighten($color-woot, 10%); + animation: spinner .9s linear infinite; + } +} + +// -------------------------------------------------------- +// arrows +// -------------------------------------------------------- +// $direction: top, left, right, bottom, top-left, top-right, bottom-left, bottom-right +// $color: hex, rgb or rbga +// $size: px or em +// @example +// .element{ +// @include arrow(top, #000, 50px); +// } +@mixin arrow($direction, $color, $size){ + display: block; + height: 0; + width: 0; + content: ''; + + @if $direction == 'top' { + border-left: $size solid transparent; + border-right: $size solid transparent; + border-bottom: $size solid $color; + } @else if $direction == 'right' { + border-top: $size solid transparent; + border-bottom: $size solid transparent; + border-left: $size solid $color; + } @else if $direction == 'bottom' { + border-top: $size solid $color; + border-right: $size solid transparent; + border-left: $size solid transparent; + } @else if $direction == 'left' { + border-top: $size solid transparent; + border-right: $size solid $color; + border-bottom: $size solid transparent; + } @else if $direction == 'top-left' { + border-top: $size solid $color; + border-right: $size solid transparent; + } @else if $direction == 'top-right' { + border-top: $size solid $color; + border-left: $size solid transparent; + } @else if $direction == 'bottom-left' { + border-bottom: $size solid $color; + border-right: $size solid transparent; + } @else if $direction == 'bottom-right' { + border-bottom: $size solid $color; + border-left: $size solid transparent; + } +} diff --git a/app/javascript/src/assets/scss/_typography.scss b/app/javascript/src/assets/scss/_typography.scss new file mode 100644 index 000000000..40d1c5a29 --- /dev/null +++ b/app/javascript/src/assets/scss/_typography.scss @@ -0,0 +1,27 @@ +.page-title { + font-size: $font-size-big; +} + +.page-sub-title { + font-size: $font-size-large; +} + +.block-title { + font-size: $font-size-medium; +} + +.sub-block-title { + font-size: $font-size-default; +} + +.text-block-title { + font-size: $font-size-small; +} + +a { + font-size: $font-size-small; +} + +p { + font-size: $font-size-small; +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/_variables.scss b/app/javascript/src/assets/scss/_variables.scss new file mode 100644 index 000000000..7eef28900 --- /dev/null +++ b/app/javascript/src/assets/scss/_variables.scss @@ -0,0 +1,82 @@ +// Font sizes +$font-size-nano: 0.8rem; +$font-size-micro: 1.0rem; +$font-size-mini: 1.2rem; +$font-size-small: 1.4rem; +$font-size-default: 1.6rem; +$font-size-medium: 1.8rem; +$font-size-large: 2.2rem; +$font-size-big: 2.4rem; +$font-size-bigger: 3.0rem; +$font-size-mega: 3.4rem; +$font-size-giga: 4.0rem; + +// spaces +$zero: 0rem; +$space-micro: 0.2rem; +$space-smaller: 0.4rem; +$space-small: 0.8rem; +$space-one: 1rem; +$space-slab: 1.2rem; +$space-normal: 1.6rem; +$space-two: 2.0rem; +$space-medium: 2.4rem; +$space-large: 3.2rem; +$space-larger: 4.8rem; +$space-jumbo: 6.4rem; +$space-mega: 10.0rem; + +// font-weight +$font-weight-feather: 100; +$font-weight-light: 300; +$font-weight-normal: 400; +$font-weight-medium: 500; +$font-weight-bold: 600; +$font-weight-black: 700; + +//Navbar +$nav-bar-width: 23rem; +$header-height: 5.6rem; + +// Woot Logo +$woot-logo-width: 20rem; +$woot-logo-height: 8rem; +$woot-logo-padding: $space-large $space-large $space-large $space-large; + +// Colors +$color-woot: #1f93ff; +$color-gray: #6E6F73; +$color-light-gray: #999A9B; +$color-border: #E0E6ED; +$color-border-light: #f0f4f5; +$color-background: #EFF2F7; +$color-background-light: #F9FAFC; +$color-white: #FFFFFF; +$color-body: #3C4858; +$color-heading: #1F2D3D; +$color-modal-header: #f1f1f1; +// Thumbnail +$thumbnail-radius: 4rem; + +// chat-header +$conv-header-height: 4rem; + +// login + +// Inbox List + +$inbox-thumb-size: 4.8rem; + + +// Spinner +$spinkit-spinner-color: $color-white !default; +$spinkit-spinner-margin: 0 0 0 1.6rem !default; +$spinkit-size: 1.6rem !default; + +// Snackbar default +$woot-snackbar-bg: #323232; +$woot-snackbar-button: #ffeb3b; + +$swift-ease-out-duration: .4s !default; +$swift-ease-out-timing-function: cubic-bezier(.25, .8, .25, 1) !default; +$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default; diff --git a/app/javascript/src/assets/scss/_woot.scss b/app/javascript/src/assets/scss/_woot.scss new file mode 100644 index 000000000..68405ee94 --- /dev/null +++ b/app/javascript/src/assets/scss/_woot.scss @@ -0,0 +1,30 @@ +@import 'typography'; +@import 'layout'; +@import 'animations'; + +@import 'foundation-custom'; +@import 'widgets/search-box'; +@import 'widgets/conv-header'; +@import 'widgets/thumbnail'; +@import 'widgets/conversation-card'; +@import 'widgets/conversation-view'; +@import 'widgets/reply-box'; +@import 'widgets/tabs'; +@import 'widgets/login'; +@import 'widgets/emojiinput'; +@import 'widgets/woot-tables'; +@import 'widgets/sidemenu'; +@import 'widgets/forms'; +@import 'widgets/buttons'; +@import 'widgets/snackbar'; +@import 'widgets/modal'; +@import 'widgets/states'; +@import 'widgets/report'; +@import 'widgets/billing'; +@import 'widgets/status-bar'; + +@import 'views/settings/inbox'; +@import 'views/settings/channel'; +@import 'views/signup'; + +@import 'plugins/multiselect'; diff --git a/app/javascript/src/assets/scss/app.scss b/app/javascript/src/assets/scss/app.scss new file mode 100644 index 000000000..fe6ea3aff --- /dev/null +++ b/app/javascript/src/assets/scss/app.scss @@ -0,0 +1,9 @@ +@import '~bourbon/app/assets/stylesheets/bourbon'; +@import 'variables'; +@import '~spinkit/scss/spinners/7-three-bounce'; +@import url('https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/css/ionicons.css'); +@import 'foundation-settings'; +@import 'mixins'; +@import 'helper-classes'; +@import '~foundation-sites/assets/foundation-flex'; +@import 'woot'; diff --git a/app/javascript/src/assets/scss/plugins/_multiselect.scss b/app/javascript/src/assets/scss/plugins/_multiselect.scss new file mode 100644 index 000000000..5c9136f79 --- /dev/null +++ b/app/javascript/src/assets/scss/plugins/_multiselect.scss @@ -0,0 +1,27 @@ +// @import '~vue-multiselect/dist/vue-multiselect.min.css'; +.multiselect { + min-height: 38px; + margin-bottom: $space-normal; + + > .multiselect__tags { + padding-top: $zero; + min-height: 38px; + border-radius: 0; + border: 1px solid $light-gray; + @include margin(0); + + .multiselect__tag { + margin-top: $space-smaller; + } + + .multiselect__input { + @include ghost-input; + margin-bottom: $zero; + @include padding($zero); + } + .multiselect__single { + @include padding($space-small); + margin-bottom: 0; + } + } +} diff --git a/app/javascript/src/assets/scss/views/_signup.scss b/app/javascript/src/assets/scss/views/_signup.scss new file mode 100644 index 000000000..5ae3d16f7 --- /dev/null +++ b/app/javascript/src/assets/scss/views/_signup.scss @@ -0,0 +1,91 @@ +.signup { + // margin-top: $space-larger*1.2; + + .signup__hero { + margin-bottom: $space-larger*1.5; + + .hero__logo { + width: 180px; + } + + .hero__title { + margin-top: $space-large; + font-weight: $font-weight-light; + } + + .hero__sub { + font-size: $font-size-medium; + color: $medium-gray; + } + } + + .signup__features { + list-style-type: none; + font-size: $font-size-medium; + + > li { + padding: $space-slab; + + > i { + margin-right: $space-two; + font-size: $font-size-large; + + &.beer { + color: #dfb63b; + } + + &.report { + color: #2196f3; + } + + &.canned { + color: #1cad22; + } + + &.uptime { + color: #a753b5; + } + + &.secure { + color: #607d8b; + } + } + } + } + + .signup-box { + @include elegant-card; + padding: $space-large $space-large; + label { + font-size: $font-size-default; + color: $color-gray; + + input { + padding: $space-slab; + height: $space-larger; + font-size: $font-size-default; + } + .error { + font-size: $font-size-small + } + } + } + + .sigin__footer { + padding: $space-medium; + font-size: $font-size-default; + + > a { + font-weight: $font-weight-bold; + } + } + + .accept-terms { + font-size: $font-size-mini; + text-align: center; + @include margin($zero); + a { + font-size: $font-size-mini; + } + } +} diff --git a/app/javascript/src/assets/scss/views/settings/channel.scss b/app/javascript/src/assets/scss/views/settings/channel.scss new file mode 100644 index 000000000..f948dab75 --- /dev/null +++ b/app/javascript/src/assets/scss/views/settings/channel.scss @@ -0,0 +1,48 @@ +.channels { + margin-top: $space-medium; + .inactive { + @include filter(grayscale(100%)); + } + .channel { + @include flex; + @include padding($space-normal $zero); + @include margin($zero); + @include background-white; + @include border-light; + @include flex-direction(column); + cursor: pointer; + border-right-color: $color-white; + @include transition(all 0.200s ease-in); + + &:last-child { + @include border-light; + } + + &:hover { + border: 1px solid $primary-color; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); + z-index: 999; + } + + &.disabled { + opacity: .6; + } + + img { + width: 50%; + @include margin($space-normal auto); + } + + .channel__title{ + font-size: $font-size-large; + text-align: center; + color: $color-body; + text-transform: capitalize; + } + + p { + width: 100%; + color: $medium-gray; + } + } +} diff --git a/app/javascript/src/assets/scss/views/settings/inbox.scss b/app/javascript/src/assets/scss/views/settings/inbox.scss new file mode 100644 index 000000000..c2ef08722 --- /dev/null +++ b/app/javascript/src/assets/scss/views/settings/inbox.scss @@ -0,0 +1,242 @@ +// Conversation header - Light BG +.settings-header { + @include padding($space-small $space-normal); + @include background-white; + @include flex; + @include flex-align($x: justify, $y: middle); + @include border-normal-bottom; + height: $header-height; + min-height: $header-height; + // Resolve Button + .button { + @include margin(0); + } + + // User thumbnail and text + .page-title { + @include flex; + @include flex-align($x: center, $y: middle); + @include margin($zero); + > span { + @include padding($zero $space-small $zero $space-small); + } + } + +} + +.wizard-box { + .item { + @include padding($space-normal $space-normal $space-normal $space-medium); + position: relative; + @include background-light; + cursor: pointer; + + &:before, + &:after { + content: ''; + position: absolute; + width: 2px; + height: 100%; + background: $color-border; + top: $space-normal; + } + + &:before { + top: $zero; + height: $space-normal; + } + + &:first-child { + &:before { + height: 0; + } + } + + &:last-child { + &:after { + height: $zero; + } + } + &.active { + // left: 1px; + // @include background-white; + // @include border-light; + // border-right: 0; + h3 { + color: $color-woot; + } + + .step { + background: $color-woot; + } + } + + &.over { + + &:after { + background: $color-woot; + } + + .step { + background: $color-woot; + } + + &+.item { + &:before { + background: $color-woot; + } + } + } + + h3 { + font-size: $font-size-default; + padding-left: $space-medium; + line-height: 1; + color: $color-body; + + .completed { + color: $success-color; + } + } + p { + font-size: $font-size-small; + color: $color-light-gray; + padding-left: $space-medium; + margin: 0; + } + .step { + position: absolute; + left: $space-normal; + top: $space-normal; + font-size: $font-size-small; + font-weight: $font-weight-medium; + background: $color-border; + border-radius: 20px; + width: $space-normal; + height: $space-normal; + text-align: center; + line-height: $space-normal; + color: #fff; + z-index: 999; + + i { + font-size: $font-size-micro; + } + } + } +} + +.wizard-body { + @include background-white; + @include padding($space-medium); + @include border-light; + @include full-height(); +} +.inoboxes-list { + // @include margin(auto); + // @include background-white; + // @include border-light; + // width: 50%; + + .inbox-item { + @include margin($space-normal); + @include flex; + @include flex-shrink; + @include padding($space-normal $space-normal); + @include border-light-bottom(); + flex-direction: column; + background: $color-white; + cursor: pointer; + width: 20%; + float: left; + min-height: 10rem; + &:last-child { + margin-bottom: $zero; + @include border-nil; + } + + &:hover { + @include background-gray; + .arrow { + opacity: 1; + @include transform(translateX($space-small)); + } + } + .switch { + align-self: center; + margin-right: $space-normal; + margin-bottom: $zero; + } + + .item--details { + @include padding($space-normal $zero); + .item--name { + font-size: $font-size-large; + line-height: 1; + } + .item--sub { + margin-bottom: 0; + font-size: $font-size-small; + } + } + .arrow { + align-self: center; + font-size: $font-size-small; + color: $medium-gray; + opacity: .7; + @include transform(translateX(0px)); + @include transition(opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s); + } + } +} + +.settings-modal { + width: 60%; + height: 80%; + .delete-wrapper { + position: absolute; + bottom: 0; + width: 100%; + @include flex; + flex-direction: row; + @include justify-content(space-between); + @include padding($space-normal $space-large); + a { + margin-left: $space-normal; + } + } + .code-wrapper { + @include margin($space-medium); + + .title { + font-weight: $font-weight-medium; + } + .code { + max-height: $space-mega; + overflow: scroll; + white-space: nowrap; + @include padding($space-one); + background: $color-background; + code { + background: transparent; + border: 0; + } + } + } + .agent-wrapper { + @include margin($space-medium); + .title { + font-weight: $font-weight-medium; + } + } +} +.login-init { + text-align: center; + padding-top: 30%; + p { + @include padding($space-medium); + } + > a > img { + width: $space-larger*5; + } +} diff --git a/app/javascript/src/assets/scss/widgets/_billing.scss b/app/javascript/src/assets/scss/widgets/_billing.scss new file mode 100644 index 000000000..5244e2c2c --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_billing.scss @@ -0,0 +1,65 @@ +.billing { + @include full-height; + .row { + @include full-height; + } + .billing__stats { + @include flex; + } + .billing__form { + @include thin-border($color-border-light); + @include margin($zero -$space-micro); + @include full-height; + background: $color-white; + iframe { + border: 0; + @include full-height; + width: 100%; + } + } + .account-row { + @include flex-grid-column(3, $space-medium); + @include padding($space-normal); + background: $color-white; + @include flex; + @include flex-direction(column); + // @include thin-border($color-border-light); + // @include margin(-$space-micro $zero); + font-size: $font-size-small; + .title { + font-weight: $font-weight-medium; + color: $color-heading; + } + .value { + font-size: $font-size-mega; + font-weight: $font-weight-light; + text-transform: capitalize; + } + } +} + +.account-locked { + @include background-gray; + @include margin(0); + .lock-message{ + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + div { + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + img { + width: 10rem; + @include margin($space-normal); + } + span { + text-align: center; + font-size: $font-size-small; + font-weight: $font-weight-medium; + } + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_buttons.scss b/app/javascript/src/assets/scss/widgets/_buttons.scss new file mode 100644 index 000000000..ec6eeb94f --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_buttons.scss @@ -0,0 +1,30 @@ +.button { + &.icon { + padding-left: $space-normal; + padding-right: $space-normal; + i { + padding-right: $space-one; + } + } + + &.nice { + border-radius: $space-smaller; + } + + &.hollow { + &.link { + border-color: transparent; + padding-left: 0; + + &:hover, + &:focus { + border-color: transparent; + } + } + } + + > .icon { + font-size: $font-size-mini; + margin-right: $space-smaller; + } +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_conv-header.scss b/app/javascript/src/assets/scss/widgets/_conv-header.scss new file mode 100644 index 000000000..eae651a1a --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_conv-header.scss @@ -0,0 +1,64 @@ +// Conversation header - Light BG +.conv-header { + @include padding($space-small $space-normal); + @include background-white; + @include flex; + @include flex-align($x: justify, $y: middle); + @include border-normal-bottom; + // Resolve Button + .button { + @include margin(0); + @include flex; + } + + .multiselect-box { + @include flex; + @include flex-align($x: justify, $y: middle); + @include margin(0 $space-small); + @include border-light; + border-radius: $space-smaller; + margin-right: $space-normal; + + &:before { + line-height: 3.8rem; + font-size: $font-size-default; + padding-left: $space-slab; + padding-right: $space-smaller; + color: $medium-gray; + } + + .multiselect { + margin: 0; + + .multiselect__tags { + border: 0; + } + } + } + + // User thumbnail and text + .user { + @include flex; + @include flex-align($x: center, $y: middle); + .user--name { + @include margin(0); + font-size: $font-size-medium; + margin-left: $space-slab; + } + } +} + +.button.resolve--button { + > .icon { + padding-right: $space-small; + font-size: $font-size-default; + } + + .spinner { + padding: 0 $space-one; + margin-right: $space-smaller; + &:before { + border-top-color: $color-white; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_conversation-card.scss b/app/javascript/src/assets/scss/widgets/_conversation-card.scss new file mode 100644 index 000000000..9fccf6c69 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_conversation-card.scss @@ -0,0 +1,89 @@ +.conversation { + @include flex; + @include flex-shrink; + @include padding($space-normal $zero $zero $space-normal); + position: relative; + cursor: pointer; + + &.active { + background: $color-background; + } + + .conversation--details { + @include margin($zero $zero $zero $space-one); + @include border-light-bottom; + @include padding($zero $zero $space-slab $zero); + } + .conversation--user { + font-size: $font-size-small; + margin-bottom: $zero; + + .label { + position: relative; + top: $space-micro; + left: $space-micro; + max-width: $space-jumbo; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .conversation--message { + height: $space-medium; + margin: $zero; + font-size: $font-size-small; + line-height: $space-medium; + font-weight: $font-weight-light; + text-overflow: ellipsis; + overflow: hidden; + color: $color-body; + width: 27rem; + white-space: nowrap; + } + .conversation--meta { + display: block; + position: absolute; + right: $space-normal; + top: $space-normal; + @include flex; + @include flex-direction(column); + + .unread { + $unread-size: $space-two - $space-micro; + display: none; + height: $unread-size; + min-width: $unread-size; + background: darken($success-color, 3%); + text-align: center; + padding: 0 $space-smaller; + line-height: $unread-size; + color: $color-white; + font-weight: $font-weight-medium; + font-size: $font-size-mini; + margin-left: auto; + @include round-corner; + margin-top: $space-smaller; + } + .timestamp { + font-size: $font-size-mini; + color: $dark-gray; + line-height: $space-normal; + font-weight: $font-weight-normal; + font-size: $font-size-micro; + margin-left: auto; + } + } + + &.unread-chat { + .unread { + display: inline-block; + } + .conversation--message { + font-weight: $font-weight-medium; + } + .conversation--user { + font-weight: $font-weight-medium; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_conversation-view.scss b/app/javascript/src/assets/scss/widgets/_conversation-view.scss new file mode 100644 index 000000000..b3117f3cd --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_conversation-view.scss @@ -0,0 +1,310 @@ +.conversations-sidebar { + @include flex; + @include flex-direction(column); + + .chat-list__top { + @include padding($space-normal $zero $space-small $zero); + .page-title { + float: left; + margin-bottom: $zero; + margin-left: $space-normal; + } + + .status--filter { + float: right; + width: auto; + font-size: $font-size-mini; + @include padding($zero null $zero $space-normal); + @include border-light; + @include round-corner; + @include margin($space-smaller $space-slab $zero $zero); + background-color: $color-background; + height: $space-medium; + } + } + + .conversations-list { + @include flex-weight(1); + @include scroll-on-hover; + } + + .content-box { + text-align: center; + } +} + +.emojione { + height: $font-size-medium; + width: $font-size-medium; +} + +.conversation-wrap { + @include background-gray; + @include margin(0); + @include border-normal-left; + .current-chat{ + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + div { + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + img { + width: 10rem; + @include margin($space-normal); + } + span { + text-align: center; + font-size: $font-size-small; + font-weight: $font-weight-medium; + } + } + } + .conv-empty-state { + @include flex; + @include full-height; + @include flex-direction(column); + @include flex-align(center, middle); + } +} + +.conversation-panel { + @include flex; + @include flex-weight(1); + @include flex-direction(column); + @include margin($zero); + overflow-y: scroll; + // FIrefox flexbox fix + height: 100%; + + > li { + @include flex; + @include flex-shrink; + @include margin($zero $zero $space-smaller); + + &:first-child { + margin-top: auto; + } + + &:last-child { + margin-bottom: $space-small; + } + + &.unread--toast { + span { + margin: $space-one auto; + padding: $space-smaller $space-two; + font-size: $font-size-mini; + font-weight: $font-weight-medium; + @include elegant-card; + @include round-corner; + background: $color-woot; + color: $color-white; + } + } + + .bubble { + text-align: left; + max-width: 50rem; + word-wrap: break-word; + .aplayer { + box-shadow: none; + font-family: inherit; + } + } + + &.left { + .bubble { + background: $white; + color: $color-heading; + margin-right: auto; + border-bottom-left-radius: 0; + border-top-left-radius: 0; + } + + &+.right { + margin-top: $space-one; + .bubble { + border-top-right-radius: $space-small; + } + } + + } + + &.right { + @include flex-align(right, null); + + .wrap { + text-align: right; + margin-right: $space-small; + } + + .bubble { + margin-left: auto; + border-bottom-right-radius: 0; + border-top-right-radius: 0; + &.is-private { + background: lighten($warning-color, 32%); + color: $color-heading; + position: relative; + padding-right: $space-large; + + &:before { + position: absolute; + top: $space-smaller + $space-micro; + right: $space-one; + bottom: 0; + color: $medium-gray; + } + } + } + + &+.left { + margin-top: $space-one; + .bubble { + border-top-left-radius: $space-small; + } + } + } + + .wrap { + @include margin($zero $space-normal); + max-width: 69%; + + .sender--name { + font-size: $font-size-mini; + margin-bottom: $space-smaller; + } + } + + .sender--thumbnail { + width: $space-slab; + height: $space-slab; + @include round-corner(); + margin-right: $space-one; + margin-top: $space-micro; + } + .activity-wrap { + @include flex; + @include margin($space-small auto); + @include padding($space-smaller $space-normal); + @include flex-align($x: center, $y: null); + font-size: $font-size-small; + background: lighten($warning-color, 32%); + border-radius: $space-smaller; + + p { + margin-bottom: $zero; + color: $color-heading; + + .ion-person { + margin-right: $space-small; + font-size: $font-size-default; + top: $space-micro; + position: relative; + color: $color-body; + } + + .message-text__wrap { + position: relative; + } + .message-text { + &:after { + content: " \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0"; + display: inline; + } + } + } + + .time { + color: $medium-gray; + } + } + + .bubble { + @include padding($space-smaller $space-one); + @include margin($zero); + background: #c7e3ff; + color: $color-heading; + border-radius: $space-small; + font-size: $font-size-small; + box-shadow: 0 0.5px 0.5px rgba(0,0,0,0.05); + position: relative; + + .icon { + position: absolute; + right: $space-small; + bottom: $space-smaller; + } + .message-text__wrap { + position: relative; + } + .message-text { + &:after { + content: " \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0"; + display: inline; + } + } + + .audio { + .time { + margin-top: -$space-two; + } + } + .image { + @include flex; + @include justify-content(center); + @include align-items(flex-end); + text-align: center; + img { + max-height: 30rem; + max-width: 20rem; + @include padding($space-small); + } + .time { + white-space: nowrap; + margin-left: -$space-large; + } + .modal-image { + max-width: 80%; + max-height: 80%; + } + } + .map { + @include flex; + flex-direction: column; + text-align: right; + img { + max-height: 30rem; + max-width: 20rem; + @include padding($space-small); + } + .time { + white-space: nowrap; + margin-top: -$space-two; + margin-left: -$space-smaller; + @include padding($space-small); + } + .locname { + font-weight: $font-weight-medium; + padding: $space-smaller; + } + } + } + + .time { + margin-left: $space-slab; + text-align: right; + font-size: $font-size-micro; + color: $color-gray; + position: absolute; + bottom: -$space-micro; + right: -$space-micro; + font-style: italic; + float: right; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_emojiinput.scss b/app/javascript/src/assets/scss/widgets/_emojiinput.scss new file mode 100644 index 000000000..02847c69c --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_emojiinput.scss @@ -0,0 +1,106 @@ +.emoji-dialog { + width: 28.6rem; + height: 20rem; + background: $color-white; + box-sizing: content-box; + border-radius: 2px; + position: absolute; + top: -22rem; + right: 0; + padding-bottom: $space-two; + @include elegant-card; + + &:before { + position: absolute; + bottom: -$space-slab; + right: $space-two; + @include arrow(bottom, $color-white, $space-slab); + } + + .emojione { + @include margin($zero); + font-size: $font-size-small; + } + + .emoji-row { + box-sizing: border-box; + overflow-y: scroll; + height: 180px; + @include padding($space-small); + padding-bottom: 0; + + .emoji { + display: inline-block; + padding: 5px; + border-radius: 4px; + } + + .emojione{ + margin: 0.6rem; + float:left; + cursor: pointer; + } + + } + .emoji-dialog-header { + @include padding($zero $space-smaller); + background-color: $light-gray; + border-top-left-radius: $space-small; + border-top-right-radius: $space-small; + + ul { + padding: 0; + margin: 0; + list-style: none; + padding-top: $space-smaller; + + li { + display: inline-block; + box-sizing: border-box; + height: 3.4rem; + text-align: center; + @include padding($space-small $space-small); + cursor: pointer; + + img, svg { + -webkit-filter: grayscale(100%); + filter: grayscale(100%); + } + + &.active { + background: #fff; + border-top-left-radius: $space-small; + border-top-right-radius: $space-small; + + img, + svg { + -webkit-filter: grayscale(0); + filter: grayscale(0); + } + } + } + + } + } + .emoji-category-title { + font-size: $font-size-small; + font-weight: $font-weight-medium; + color: $color-heading; + text-transform: capitalize; + margin: 0; + } + .emoji-category-heading-decoration { + text-align: right; + } + +} + +.emoji-dialog .emoji-category-header > * { + display: table-cell; + vertical-align: middle; +} + +.emoji-dialog .emoji-row +.emoji-dialog .emoji-row .emoji:hover { + background: #F5F7F9; +} diff --git a/app/javascript/src/assets/scss/widgets/_forms.scss b/app/javascript/src/assets/scss/widgets/_forms.scss new file mode 100644 index 000000000..b394ba3b8 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_forms.scss @@ -0,0 +1,31 @@ +.error { + #{$all-text-inputs}, + .multiselect > .multiselect__tags { + @include thin-border( darken(get-color(alert), 25%)); + } +} + +.error { + .message { + display: block; + width: 100%; + margin-top: -$space-normal; + margin-bottom: $space-one; + color: darken(get-color(alert), 25%); + font-weight: $font-weight-normal; + } +} + +.button, +textarea, +input { + &:focus { + outline: none; + } +} + +.input-wrap { + font-size: $font-size-small; + color: $color-heading; + font-weight: $font-weight-medium; +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_login.scss b/app/javascript/src/assets/scss/widgets/_login.scss new file mode 100644 index 000000000..f793d8ca7 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_login.scss @@ -0,0 +1,62 @@ +.auth-wrap { + width: 100%; +} + +// Outside login wrapper +.login { + @include full-height; + overflow-y: scroll; + padding-top: $space-larger*1.2; + + .login__hero { + margin-bottom: $space-larger; + + .hero__logo { + width: 180px; + } + + .hero__title { + margin-top: $space-larger; + font-weight: $font-weight-light; + } + + .hero__sub { + font-size: $font-size-medium; + color: $medium-gray; + } + } + + // Login box + .login-box { + @include background-white; + @include border-normal; + @include border-top-radius($space-smaller); + @include border-right-radius($space-smaller); + @include border-bottom-radius($space-smaller); + @include border-left-radius($space-smaller); + @include elegant-card; + padding: $space-large $space-large; + label { + font-size: $font-size-default; + color: $color-gray; + + input { + padding: $space-slab; + height: $space-larger; + font-size: $font-size-default; + } + .error { + font-size: $font-size-small; + } + } + } + + .sigin__footer { + padding: $space-medium; + font-size: $font-size-default; + + > a { + font-weight: $font-weight-bold; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_modal.scss b/app/javascript/src/assets/scss/widgets/_modal.scss new file mode 100644 index 000000000..c35006d8b --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_modal.scss @@ -0,0 +1,92 @@ +.modal-mask { + position: fixed; + z-index: 9990; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, .5); + @include flex; + @include flex-align(center, middle); + +} + +.modal-container { + width: 60rem; + max-height: 100%; + overflow: scroll; + position: relative; + background-color: $color-white; + + .modal--close { + font-size: $font-size-large; + position: absolute; + right: $space-normal; + top: $space-small; + cursor: pointer; + color: $color-heading; + } + + .page-top-bar { + background: $color-modal-header; + text-align: center; + @include padding($space-large $space-medium); + img { + max-height: 6rem; + } + } + + .content-box { + @include padding($zero); + height: auto; + } + + + h2 { + font-size: $font-size-medium; + color: $color-woot; + font-weight: $font-weight-normal; + @include padding($space-small $zero $zero $zero); + } + + p { + font-size: $font-size-small; + @include padding($zero); + @include margin($zero); + } + + form { + align-self: center; + @include padding($space-medium $space-larger $space-small); + a { + @include padding($space-normal); + } + } + + .modal-footer { + @include flex; + @include flex-align($x: justify, $y: center); + @include padding($space-small $zero $space-medium $zero); + button { + font-size: $font-size-small; + } + } + + .delete-item { + @include padding($space-normal); + button { + @include margin($zero); + } + } + +} + +.modal-enter, .modal-leave { + opacity: 0; +} + +.modal-enter .modal-container, +.modal-leave .modal-container { + -webkit-transform: scale(1.1); + transform: scale(1.1); +} diff --git a/app/javascript/src/assets/scss/widgets/_reply-box.scss b/app/javascript/src/assets/scss/widgets/_reply-box.scss new file mode 100644 index 000000000..a36c9f87e --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_reply-box.scss @@ -0,0 +1,143 @@ +.reply-box { + margin: $space-normal; + margin-top: 0; + border-bottom: 0; + @include elegant-card; + @include transition(height 2s $ease-in-out-cubic); + max-height: $space-jumbo*2; + + .reply-box__top { + @include flex; + @include flex-align($x: left, $y: middle); + @include padding($space-one $space-normal); + @include background-white; + @include margin(0); + position: relative; + border-top-left-radius: $space-small; + border-top-right-radius: $space-small; + + .canned { + @include elegant-card; + z-index: 100; + position: absolute; + background: #fff; + width: 24rem; + left: 0; + border-top: $space-small solid $color-white; + border-bottom: $space-small solid $color-white; + max-height: 14rem; + overflow: scroll; + .active { + a { + background: $color-woot; + } + } + } + &.is-active { + border-bottom-left-radius: $space-small; + border-bottom-right-radius: $space-small; + } + + &.is-private { + background: lighten($warning-color, 38%); + + > input { + background: lighten($warning-color, 38%); + } + } + + > .icon { + font-size: $font-size-medium; + color: $medium-gray; + margin-right: $space-small; + cursor: pointer; + + &.active { + color: $color-woot; + } + } + > textarea { + @include ghost-input(); + @include margin(0); + resize: none; + background: transparent; + // Override min-height : 50px in foundation + // + min-height: 1rem; + } + } + + .reply-box__bottom { + @include background-light; + @include flex; + @include flex-align($x: justify, $y: middle); + @include border-light-top; + border-bottom-left-radius: $space-small; + border-bottom-right-radius: $space-small; + + .tabs { + border: 0; + padding: 0; + flex: 1; + + .tabs-title { + margin: 0; + @include transition(background .2s $ease-in-out-cubic); + @include transition(color .2s $ease-in-out-cubic); + + a { + padding: $space-one $space-two; + } + + &:first-child { + border-bottom-left-radius: $space-small; + &.is-active { + @include border-light-right; + border-left: 0; + a { + border-bottom-left-radius: $space-small; + } + } + } + + &.is-private { + &.is-active { + background: lighten($warning-color, 38%); + a { + border-bottom-color: darken($warning-color, 15%); + color: darken($warning-color, 15%); + } + } + } + } + .is-active { + @include background-white; + margin-top: -1px; + @include border-light-left; + @include border-light-right; + } + + .message-length { + float: right; + a { + font-size: $font-size-mini; + } + } + .message-error { + color: $input-error-color; + } + } + + .send-button { + height: 3.6rem; + border-bottom-right-radius: $space-small; + padding-top: $space-small; + padding-right: $space-two; + padding-left: $space-two; + + .icon { + margin-left: $space-small; + } + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_report.scss b/app/javascript/src/assets/scss/widgets/_report.scss new file mode 100644 index 000000000..f39231ae0 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_report.scss @@ -0,0 +1,51 @@ +.report-card { + @include padding($space-normal $space-small $space-normal $space-two); + @include margin($zero); + @include background-light; + cursor: pointer; + @include custom-border-top(3px, transparent); + &.active { + @include custom-border-top(3px, $color-woot); + @include background-white; + .heading, + .metric { + color: $color-woot; + } + } + .heading { + @include margin($zero); + font-size: $font-size-small; + font-weight: $font-weight-bold; + color: $color-heading; + } + .metric { + font-size: $font-size-bigger; + font-weight: $font-weight-feather; + } + .desc { + @include margin($zero); + font-size: $font-size-small; + text-transform: capitalize; + } +} + + +.report-bar { + @include margin(-1px $zero); + @include background-white; + @include border-light; + @include padding($space-small $space-medium); + .chart-container { + @include flex; + @include flex-direction(column); + @include flex-align(center, middle); + div { + width: 100%; + } + .empty-state { + @include margin($space-jumbo); + font-size: $font-size-default; + color: $color-gray; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_search-box.scss b/app/javascript/src/assets/scss/widgets/_search-box.scss new file mode 100644 index 000000000..d834b607e --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_search-box.scss @@ -0,0 +1,15 @@ +.search { + @include flex; + @include flex-align($x: left, $y: middle); + @include padding($space-one $space-normal); + @include flex-shrink; + @include transition(all .3s $ease-in-out-quad); + > .icon { + font-size: $font-size-medium; + color: $medium-gray; + } + > input { + @include ghost-input(); + @include margin(0); + } +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_sidemenu.scss b/app/javascript/src/assets/scss/widgets/_sidemenu.scss new file mode 100644 index 000000000..f0b3ac4d9 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_sidemenu.scss @@ -0,0 +1,120 @@ +.side-menu { + i { + min-width: 2rem; + } +} + + +.sidebar { + width: $nav-bar-width; + z-index: 1024 - 1; + overflow-x: hidden; + @include border-normal-right; + @include background-white; + @include full-height; + @include margin(0); + @include space-between-column; + + //logo + .logo { + img { + // width: $woot-logo-width; + // height: $woot-logo-height; + @include padding($woot-logo-padding); + } + } + + .main-nav { + @include flex-weight(1); + padding: 0 $space-medium - $space-one; + @include scroll-on-hover; + + a { + color: $color-gray; + font-size: $font-size-default; + border-radius: $space-smaller; + &:before { + margin-right: $space-slab; + } + } + + .menu-title { + font-size: $font-size-medium; + color: $color-gray; + margin-top: $space-medium; + > span { + margin-left: auto; + } + & + ul > li > a{ + @include padding($space-micro null); + line-height: $global-lineheight; + color: $medium-gray; + } + } + } + + // bottom-nav + .bottom-nav { + @include flex; + @include space-between-column; + @include padding($space-one $space-normal $space-one $space-one); + @include flex-direction(column); + @include border-normal-top; + position: relative; + + .current-user { + @include flex; + @include flex-direction(row); + cursor: pointer; + + .current-user--thumbnail { + width: $space-large; + height: $space-large; + @include round-corner(); + } + .current-user--data { + @include flex; + @include flex-direction(column); + .current-user--name { + font-size: $font-size-small; + font-weight: $font-weight-medium; + margin-bottom: $zero; + margin-left: $space-one; + margin-top: $space-micro; + line-height: 1; + } + .current-user--role { + font-size: $font-size-mini; + margin-left: $space-one; + margin-bottom: $zero; + color: $color-gray; + } + } + .current-user--options { + margin-left: auto; + font-size: $font-size-big; + margin-top: auto; + margin-bottom: auto; + } + } + + .dropdown-pane { + @include elegant-card; + top: -160%; + left: 18%; + width: 80%; + @include border-light; + z-index: 999; + visibility: visible; + &:before { + position: absolute; + bottom: -$space-slab; + right: $space-slab; + @include arrow(bottom, $color-white, $space-slab); + } + } + .active { + border-bottom: 2px solid $medium-gray; + } + } +} diff --git a/app/javascript/src/assets/scss/widgets/_snackbar.scss b/app/javascript/src/assets/scss/widgets/_snackbar.scss new file mode 100644 index 000000000..5ed310018 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_snackbar.scss @@ -0,0 +1,46 @@ +.ui-snackbar-container { + position: absolute; + overflow: hidden; + z-index: 9999; + top: $space-normal; + left: $space-normal; + width: 100%; + text-align: center; +} + +.ui-snackbar { + text-align: left; + display: inline-block; + min-width: 24rem; + max-width: 40rem; + min-height: 3rem; + background-color: $woot-snackbar-bg; + @include padding($space-slab $space-medium); + @include border-top-radius($space-micro); + @include border-right-radius($space-micro); + @include border-bottom-radius($space-micro); + @include border-left-radius($space-micro); + margin-bottom: $space-small; + + // box-shadow: 0 1px 3px alpha(black, 0.12), 0 1px 2px alpha(black, 0.24); +} + +.ui-snackbar-text { + font-size: $font-size-small; + color: $color-white; +} + +.ui-snackbar-action { + margin-left: auto; + padding-left: 3rem; + + button { + border: none; + background: none; + font-size: $font-size-small; + text-transform: uppercase; + color: $woot-snackbar-button; + @include margin(0); + @include padding(0); + } +} diff --git a/app/javascript/src/assets/scss/widgets/_states.scss b/app/javascript/src/assets/scss/widgets/_states.scss new file mode 100644 index 000000000..7c32b9dc0 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_states.scss @@ -0,0 +1,39 @@ +.loading-state { + padding: $space-jumbo $space-smaller; + .message { + display: block; + width: 100%; + text-align: center; + color: $color-gray; + } + .spinner { + float: none; + top: -$space-smaller; + } +} + +// EMPTY STATES +.empty-state { + padding: $space-jumbo $space-smaller; + .title, + .message { + display: block; + text-align: center; + width: 100%; + } + + .title { + font-size: $font-size-giga; + font-weight: $font-weight-feather; + } + + .message { + width: 50%; + margin: 0 auto; + color: $color-gray; + } + + .button { + margin-top: $space-medium; + } +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_status-bar.scss b/app/javascript/src/assets/scss/widgets/_status-bar.scss new file mode 100644 index 000000000..9cac66a8b --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_status-bar.scss @@ -0,0 +1,31 @@ +.status-bar { + @include flex; + @include flex-direction(column); + @include flex-align($x: center, $y: middle); + background: lighten($warning-color, 36%); + // @include elegant-card(); + @include margin($zero); + @include padding($space-normal $space-smaller); + + .message { + font-weight: $font-weight-medium; + margin-bottom: $zero; + } + + .button { + @include margin($space-smaller $zero $zero); + padding: $space-small $space-normal; + } + + &.danger { + background: lighten($alert-color, 30%); + + .button { + @include button-style($alert-color, darken($alert-color, 7%), $color-white); + } + } + + &.warning { + background: lighten($warning-color, 36%); + } +} diff --git a/app/javascript/src/assets/scss/widgets/_tabs.scss b/app/javascript/src/assets/scss/widgets/_tabs.scss new file mode 100644 index 000000000..e6bc30bd8 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_tabs.scss @@ -0,0 +1,50 @@ +.tabs { + border-width: 0; + @include padding($zero $space-normal); + @include border-normal-bottom; + + .tabs-title { + @include margin($zero $space-one); + + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + &:hover, + &:focus{ + a { + color: darken($medium-gray, 20%); + } + } + + a { + color: $medium-gray; + border-bottom: 2px solid transparent; + font-size: $font-size-small; + @include position(relative, 1px null null null); + @include transition(all .15s $ease-in-out-cubic); + } + + &.is-active { + a { + color: $color-woot; + border-bottom-color: $color-woot; + } + } + } + + // tab chat type + &.tab--chat-type { + @include flex; + + .tabs-title { + a { + font-size: $font-size-default; + padding-top: $space-slab; + padding-bottom: $space-slab; + } + } + } +} \ No newline at end of file diff --git a/app/javascript/src/assets/scss/widgets/_thumbnail.scss b/app/javascript/src/assets/scss/widgets/_thumbnail.scss new file mode 100644 index 000000000..1196268a4 --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_thumbnail.scss @@ -0,0 +1,14 @@ +.user-thumbnail-box { + position: relative; + @include flex-shrink; + .user-thumbnail { + border-radius: 50%; + } + .source-badge { + position: absolute; + height: $space-slab; + width: $space-slab; + right: $zero; + bottom: -$space-micro/2; + } +} diff --git a/app/javascript/src/assets/scss/widgets/_woot-tables.scss b/app/javascript/src/assets/scss/widgets/_woot-tables.scss new file mode 100644 index 000000000..cd8b8150a --- /dev/null +++ b/app/javascript/src/assets/scss/widgets/_woot-tables.scss @@ -0,0 +1,50 @@ +table { + font-size: $font-size-small; + border-spacing: 0; + thead { + th { + font-weight: $font-weight-bold; + text-transform: uppercase; + } + } + tbody { + td { + @include padding($space-one $space-small); + border-bottom: 1px solid $color-border-light; + } + } +} + +.woot-table { + + tr { + .show-if-hover { + opacity: 0; + @include transition(all .2s $ease-in-out-cubic); + } + &:hover { + .show-if-hover { + opacity: 1; + } + } + } + .agent-name { + font-weight: $font-weight-medium; + display: block; + text-transform: capitalize; + } + .woot-thumbnail { + border-radius: 50%; + width: 5rem; + height: 5rem; + } + .button-wrapper { + min-width: 20rem; + @include flex; + @include flex-align(left, null); + @include flex-direction(row); + } + .button { + @include margin($zero); + } +} diff --git a/app/javascript/src/components/ChatList.vue b/app/javascript/src/components/ChatList.vue new file mode 100644 index 000000000..eb408c84a --- /dev/null +++ b/app/javascript/src/components/ChatList.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/app/javascript/src/components/Modal.vue b/app/javascript/src/components/Modal.vue new file mode 100644 index 000000000..774ec99a4 --- /dev/null +++ b/app/javascript/src/components/Modal.vue @@ -0,0 +1,33 @@ + + + diff --git a/app/javascript/src/components/ModalHeader.vue b/app/javascript/src/components/ModalHeader.vue new file mode 100644 index 000000000..1019640bb --- /dev/null +++ b/app/javascript/src/components/ModalHeader.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/javascript/src/components/Snackbar.vue b/app/javascript/src/components/Snackbar.vue new file mode 100644 index 000000000..62673e02a --- /dev/null +++ b/app/javascript/src/components/Snackbar.vue @@ -0,0 +1,30 @@ + + + diff --git a/app/javascript/src/components/SnackbarContainer.vue b/app/javascript/src/components/SnackbarContainer.vue new file mode 100644 index 000000000..37a365015 --- /dev/null +++ b/app/javascript/src/components/SnackbarContainer.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/javascript/src/components/Spinner.vue b/app/javascript/src/components/Spinner.vue new file mode 100644 index 000000000..b90ad471d --- /dev/null +++ b/app/javascript/src/components/Spinner.vue @@ -0,0 +1,3 @@ + diff --git a/app/javascript/src/components/Thumbnail.vue b/app/javascript/src/components/Thumbnail.vue new file mode 100644 index 000000000..19f293abb --- /dev/null +++ b/app/javascript/src/components/Thumbnail.vue @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/app/javascript/src/components/buttons/FormSubmitButton.vue b/app/javascript/src/components/buttons/FormSubmitButton.vue new file mode 100644 index 000000000..6b4e69a72 --- /dev/null +++ b/app/javascript/src/components/buttons/FormSubmitButton.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/javascript/src/components/buttons/ResolveButton.vue b/app/javascript/src/components/buttons/ResolveButton.vue new file mode 100644 index 000000000..6e6e28990 --- /dev/null +++ b/app/javascript/src/components/buttons/ResolveButton.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/app/javascript/src/components/index.js b/app/javascript/src/components/index.js new file mode 100644 index 000000000..3f613e544 --- /dev/null +++ b/app/javascript/src/components/index.js @@ -0,0 +1,40 @@ +/* eslint no-plusplus: 0 */ +/* eslint-env browser */ + +import Modal from './Modal'; +import Thumbnail from './Thumbnail'; +import Spinner from './Spinner'; +import SubmitButton from './buttons/FormSubmitButton'; +import Tabs from './ui/Tabs/Tabs'; +import TabsItem from './ui/Tabs/TabsItem'; +import LoadingState from './widgets/LoadingState'; +import ReportStatsCard from './widgets/ReportStatsCard'; +import Bar from './widgets/chart/BarChart'; +import ModalHeader from './ModalHeader'; + +const WootUIKit = { + Modal, + Thumbnail, + Spinner, + SubmitButton, + Tabs, + TabsItem, + LoadingState, + ReportStatsCard, + Bar, + ModalHeader, + install(Vue) { + const keys = Object.keys(this); + keys.pop(); // remove 'install' from keys + let i = keys.length; + while (i--) { + Vue.component(`woot${keys[i]}`, this[keys[i]]); + } + }, +}; + +if (typeof window !== 'undefined' && window.Vue) { + window.Vue.use(WootUIKit); +} + +export default WootUIKit; diff --git a/app/javascript/src/components/layout/Sidebar.vue b/app/javascript/src/components/layout/Sidebar.vue new file mode 100644 index 000000000..dee22c1a3 --- /dev/null +++ b/app/javascript/src/components/layout/Sidebar.vue @@ -0,0 +1,140 @@ + + + diff --git a/app/javascript/src/components/layout/SidebarItem.vue b/app/javascript/src/components/layout/SidebarItem.vue new file mode 100644 index 000000000..eaf2361da --- /dev/null +++ b/app/javascript/src/components/layout/SidebarItem.vue @@ -0,0 +1,65 @@ + + + diff --git a/app/javascript/src/components/ui/Switch.vue b/app/javascript/src/components/ui/Switch.vue new file mode 100644 index 000000000..f28c84998 --- /dev/null +++ b/app/javascript/src/components/ui/Switch.vue @@ -0,0 +1,49 @@ + + + + diff --git a/app/javascript/src/components/ui/Tabs/Tabs.js b/app/javascript/src/components/ui/Tabs/Tabs.js new file mode 100644 index 000000000..30e0b487c --- /dev/null +++ b/app/javascript/src/components/ui/Tabs/Tabs.js @@ -0,0 +1,27 @@ +/* eslint no-unused-vars: ["error", { "args": "none" }] */ + +export default { + name: 'WootTabs', + props: { + index: { + type: Number, + default: 0, + }, + }, + render(h) { + const Tabs = this.$slots.default.filter(node => + node.componentOptions && node.componentOptions.tag === 'woot-tabs-item' + ).map((node, index) => { + const data = node.componentOptions.propsData; + data.index = index; + return node; + }); + return ( +
    + { Tabs } +
+ ); + }, +}; diff --git a/app/javascript/src/components/ui/Tabs/TabsItem.js b/app/javascript/src/components/ui/Tabs/TabsItem.js new file mode 100644 index 000000000..d6c7a6ee5 --- /dev/null +++ b/app/javascript/src/components/ui/Tabs/TabsItem.js @@ -0,0 +1,84 @@ +/* eslint no-unused-vars: ["error", { "args": "none" }] */ +/* eslint prefer-template: 0 */ +/* eslint no-console: 0 */ +/* eslint func-names: 0 */ +import TWEEN from 'tween.js'; + +export default { + name: 'WootTabsItem', + props: { + index: { + type: Number, + default: 0, + }, + name: { + type: String, + required: true, + }, + disabled: { + type: Boolean, + default: false, + }, + count: { + type: Number, + default: 0, + }, + }, + + data() { + return { + animatedNumber: 0, + }; + }, + + computed: { + active() { + return this.index === this.$parent.index; + }, + + getItemCount() { + return this.animatedNumber || this.count; + }, + }, + + watch: { + count(newValue, oldValue) { + let animationFrame; + const animate = (time) => { + TWEEN.update(time); + animationFrame = window.requestAnimationFrame(animate); + }; + const that = this; + new TWEEN.Tween({ tweeningNumber: oldValue }) + .easing(TWEEN.Easing.Quadratic.Out) + .to({ tweeningNumber: newValue }, 500) + .onUpdate(function () { + that.animatedNumber = this.tweeningNumber.toFixed(0); + }) + .onComplete(() => { + window.cancelAnimationFrame(animationFrame); + }) + .start(); + animationFrame = window.requestAnimationFrame(animate); + }, + }, + + render(h) { + return ( +
  • + { + event.preventDefault(); + if (!this.disabled) { + this.$parent.$emit('change', this.index); + } + }}> + { `${this.name} (${this.getItemCount})` } + +
  • + ); + }, +}; diff --git a/app/javascript/src/components/ui/Wizard.vue b/app/javascript/src/components/ui/Wizard.vue new file mode 100644 index 000000000..5eca6a4a5 --- /dev/null +++ b/app/javascript/src/components/ui/Wizard.vue @@ -0,0 +1,43 @@ + + + + diff --git a/app/javascript/src/components/widgets/BackButton.vue b/app/javascript/src/components/widgets/BackButton.vue new file mode 100644 index 000000000..9b5085983 --- /dev/null +++ b/app/javascript/src/components/widgets/BackButton.vue @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/app/javascript/src/components/widgets/ChannelItem.vue b/app/javascript/src/components/widgets/ChannelItem.vue new file mode 100644 index 000000000..13ca1f2ce --- /dev/null +++ b/app/javascript/src/components/widgets/ChannelItem.vue @@ -0,0 +1,23 @@ + + diff --git a/app/javascript/src/components/widgets/ChatTypeTabs.vue b/app/javascript/src/components/widgets/ChatTypeTabs.vue new file mode 100644 index 000000000..4812a599b --- /dev/null +++ b/app/javascript/src/components/widgets/ChatTypeTabs.vue @@ -0,0 +1,27 @@ + + diff --git a/app/javascript/src/components/widgets/EmptyState.vue b/app/javascript/src/components/widgets/EmptyState.vue new file mode 100644 index 000000000..15b8e345a --- /dev/null +++ b/app/javascript/src/components/widgets/EmptyState.vue @@ -0,0 +1,17 @@ + + diff --git a/app/javascript/src/components/widgets/InboxListItem.vue b/app/javascript/src/components/widgets/InboxListItem.vue new file mode 100644 index 000000000..bc9ed1daa --- /dev/null +++ b/app/javascript/src/components/widgets/InboxListItem.vue @@ -0,0 +1,21 @@ + + diff --git a/app/javascript/src/components/widgets/LoadingState.vue b/app/javascript/src/components/widgets/LoadingState.vue new file mode 100644 index 000000000..f8d33c961 --- /dev/null +++ b/app/javascript/src/components/widgets/LoadingState.vue @@ -0,0 +1,12 @@ + + diff --git a/app/javascript/src/components/widgets/ReportStatsCard.vue b/app/javascript/src/components/widgets/ReportStatsCard.vue new file mode 100644 index 000000000..b8a0f19c7 --- /dev/null +++ b/app/javascript/src/components/widgets/ReportStatsCard.vue @@ -0,0 +1,20 @@ + + diff --git a/app/javascript/src/components/widgets/SearchBox.vue b/app/javascript/src/components/widgets/SearchBox.vue new file mode 100644 index 000000000..773579e10 --- /dev/null +++ b/app/javascript/src/components/widgets/SearchBox.vue @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/javascript/src/components/widgets/StatusBar.vue b/app/javascript/src/components/widgets/StatusBar.vue new file mode 100644 index 000000000..902c2515e --- /dev/null +++ b/app/javascript/src/components/widgets/StatusBar.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/javascript/src/components/widgets/Thumbnail.vue b/app/javascript/src/components/widgets/Thumbnail.vue new file mode 100644 index 000000000..ac1f3f5d4 --- /dev/null +++ b/app/javascript/src/components/widgets/Thumbnail.vue @@ -0,0 +1,29 @@ + + \ No newline at end of file diff --git a/app/javascript/src/components/widgets/chart/BarChart.js b/app/javascript/src/components/widgets/chart/BarChart.js new file mode 100644 index 000000000..310367dbb --- /dev/null +++ b/app/javascript/src/components/widgets/chart/BarChart.js @@ -0,0 +1,35 @@ +import { Bar } from 'vue-chartjs'; + +const fontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; + +export default Bar.extend({ + props: ['collection'], + mounted() { + this.renderChart(this.collection, { + // responsive: true, + maintainAspectRatio: false, + legend: { + labels: { + fontFamily, + }, + }, + scales: { + xAxes: [ + { + barPercentage: 1.9, + ticks: { + fontFamily, + }, + }, + ], + yAxes: [ + { + ticks: { + fontFamily, + }, + }, + ], + }, + }); + }, +}); diff --git a/app/javascript/src/components/widgets/conversation/CannedResponse.vue b/app/javascript/src/components/widgets/conversation/CannedResponse.vue new file mode 100644 index 000000000..ee07b4393 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/CannedResponse.vue @@ -0,0 +1,84 @@ + + + diff --git a/app/javascript/src/components/widgets/conversation/ChatFilter.vue b/app/javascript/src/components/widgets/conversation/ChatFilter.vue new file mode 100644 index 000000000..589b38a02 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ChatFilter.vue @@ -0,0 +1,22 @@ + + + + diff --git a/app/javascript/src/components/widgets/conversation/Conversation.vue b/app/javascript/src/components/widgets/conversation/Conversation.vue new file mode 100644 index 000000000..de5be0408 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/Conversation.vue @@ -0,0 +1,97 @@ + + diff --git a/app/javascript/src/components/widgets/conversation/ConversationBox.vue b/app/javascript/src/components/widgets/conversation/ConversationBox.vue new file mode 100644 index 000000000..6ad5f37e0 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ConversationBox.vue @@ -0,0 +1,202 @@ + + + + diff --git a/app/javascript/src/components/widgets/conversation/ConversationCard.vue b/app/javascript/src/components/widgets/conversation/ConversationCard.vue new file mode 100644 index 000000000..9c7460afa --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ConversationCard.vue @@ -0,0 +1,93 @@ + + diff --git a/app/javascript/src/components/widgets/conversation/ConversationHeader.vue b/app/javascript/src/components/widgets/conversation/ConversationHeader.vue new file mode 100644 index 000000000..cddaf02e7 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ConversationHeader.vue @@ -0,0 +1,95 @@ + + diff --git a/app/javascript/src/components/widgets/conversation/ReplyBox.vue b/app/javascript/src/components/widgets/conversation/ReplyBox.vue new file mode 100644 index 000000000..fca785e51 --- /dev/null +++ b/app/javascript/src/components/widgets/conversation/ReplyBox.vue @@ -0,0 +1,207 @@ +