diff --git a/.babelrc b/.babelrc
index ded31c0d80..75e4c04e4a 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,18 +1,26 @@
{
"presets": [
- ["env", {
- "modules": false,
- "targets": {
- "browsers": "> 1%",
- "uglify": true
- },
- "useBuiltIns": true
- }]
+ [
+ "env",
+ {
+ "modules": false,
+ "targets": {
+ "browsers": "> 1%",
+ "uglify": true
+ },
+ "useBuiltIns": true
+ }
+ ],
+ "react"
],
-
"plugins": [
"syntax-dynamic-import",
"transform-object-rest-spread",
- ["transform-class-properties", { "spec": true }]
+ [
+ "transform-class-properties",
+ {
+ "spec": true
+ }
+ ]
]
}
diff --git a/.gitignore b/.gitignore
index bcc7cb5fd3..dbb56137e9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,9 @@ dump.rdb
yarn-error.log
.DS_Store
+# Ignore assets stored in /storage
+/storage
+
# Ignore Byebug command history file.
.byebug_history
/ssl/
diff --git a/Gemfile b/Gemfile
index 25b0df6111..df4a3f49fa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -19,6 +19,9 @@ gem "attr_encrypted", "~> 3.1.0"
# Integration with Matomo Piwik
gem 'autometal-piwik', :require => 'piwik', git: "https://github.com/matomo-org/piwik-ruby-api.git", branch: "master"
+# Use AWS gem for s3 uploads
+gem 'aws-sdk-s3', require: false
+
gem "bootstrap", "~> 4.1.1"
# browser details
@@ -49,6 +52,9 @@ gem "nokogiri", "~> 1.8.4"
# Open Graph tag
gem "meta-tags"
+# Image conversion library
+gem 'mini_magick'
+
# Oauth client for google / youtube
gem "omniauth-google-oauth2", "~> 0.5.2"
diff --git a/Gemfile.lock b/Gemfile.lock
index bae4158ab7..e3f22771fa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -78,6 +78,21 @@ GEM
encryptor (~> 3.0.0)
autoprefixer-rails (9.0.0)
execjs
+ aws-eventstream (1.0.1)
+ aws-partitions (1.104.0)
+ aws-sdk-core (3.27.0)
+ aws-eventstream (~> 1.0)
+ aws-partitions (~> 1.0)
+ aws-sigv4 (~> 1.0)
+ jmespath (~> 1.0)
+ aws-sdk-kms (1.9.0)
+ aws-sdk-core (~> 3, >= 3.26.0)
+ aws-sigv4 (~> 1.0)
+ aws-sdk-s3 (1.19.0)
+ aws-sdk-core (~> 3, >= 3.26.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.0)
+ aws-sigv4 (1.0.3)
bcrypt (3.1.12)
bindex (0.5.0)
bootstrap (4.1.3)
@@ -106,7 +121,7 @@ GEM
activesupport
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
- chromedriver-helper (1.2.0)
+ chromedriver-helper (2.0.1)
archive-zip (~> 0.10)
nokogiri (~> 1.8)
chunky_png (1.3.10)
@@ -170,6 +185,7 @@ GEM
jbuilder (2.7.0)
activesupport (>= 4.2.0)
multi_json (>= 1.2)
+ jmespath (1.4.0)
json (2.1.0)
jsonapi-renderer (0.2.0)
jwt (1.5.6)
@@ -196,6 +212,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
mimemagic (0.3.2)
+ mini_magick (4.9.2)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.11.3)
@@ -457,6 +474,7 @@ DEPENDENCIES
api-pagination
attr_encrypted (~> 3.1.0)
autometal-piwik!
+ aws-sdk-s3
bootstrap (~> 4.1.1)
brakeman
browser
@@ -476,6 +494,7 @@ DEPENDENCIES
listen (~> 3.0.5)
lograge (~> 0.4)
meta-tags
+ mini_magick
minitest-rails-capybara (~> 3.0.1)
mocha
newrelic_rpm (~> 3.16)
diff --git a/app/assets/images/.DS_Store b/app/assets/images/.DS_Store
deleted file mode 100644
index 8fe46f6bdc..0000000000
Binary files a/app/assets/images/.DS_Store and /dev/null differ
diff --git a/app/assets/images/bg_bats.svg b/app/assets/images/bg_bats.svg
new file mode 100644
index 0000000000..b0318e87a0
--- /dev/null
+++ b/app/assets/images/bg_bats.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/bg_hearts.svg b/app/assets/images/bg_hearts.svg
new file mode 100644
index 0000000000..30f21597b0
--- /dev/null
+++ b/app/assets/images/bg_hearts.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/brave-lion@3x.jpg b/app/assets/images/brave-lion@3x.jpg
new file mode 100644
index 0000000000..59fec8c30c
Binary files /dev/null and b/app/assets/images/brave-lion@3x.jpg differ
diff --git a/app/assets/images/camera.png b/app/assets/images/camera.png
new file mode 100644
index 0000000000..746c40375b
Binary files /dev/null and b/app/assets/images/camera.png differ
diff --git a/app/assets/images/camera@2x.png b/app/assets/images/camera@2x.png
new file mode 100644
index 0000000000..cb0e904c4e
Binary files /dev/null and b/app/assets/images/camera@2x.png differ
diff --git a/app/assets/images/camera@3x.png b/app/assets/images/camera@3x.png
new file mode 100644
index 0000000000..6eb8b80f79
Binary files /dev/null and b/app/assets/images/camera@3x.png differ
diff --git a/app/assets/images/icn-donation-jar@1x.png b/app/assets/images/icn-donation-jar@1x.png
new file mode 100644
index 0000000000..c1f26a67e3
Binary files /dev/null and b/app/assets/images/icn-donation-jar@1x.png differ
diff --git a/app/assets/images/icn-donation-jar@2x.png b/app/assets/images/icn-donation-jar@2x.png
new file mode 100644
index 0000000000..9e1598b517
Binary files /dev/null and b/app/assets/images/icn-donation-jar@2x.png differ
diff --git a/app/assets/images/icn-donation-jar@3x.png b/app/assets/images/icn-donation-jar@3x.png
new file mode 100644
index 0000000000..d452cad983
Binary files /dev/null and b/app/assets/images/icn-donation-jar@3x.png differ
diff --git a/app/assets/images/icn-editPhoto.svg b/app/assets/images/icn-editPhoto.svg
new file mode 100644
index 0000000000..97f52cd53b
--- /dev/null
+++ b/app/assets/images/icn-editPhoto.svg
@@ -0,0 +1,18 @@
+
+
+
+ camera2
+ Created with Sketch.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/assets/images/san_francisco.jpg b/app/assets/images/san_francisco.jpg
new file mode 100644
index 0000000000..384e779176
Binary files /dev/null and b/app/assets/images/san_francisco.jpg differ
diff --git a/app/assets/stylesheets/pages/home.scss b/app/assets/stylesheets/pages/home.scss
index 3369b34058..e2a414d308 100644
--- a/app/assets/stylesheets/pages/home.scss
+++ b/app/assets/stylesheets/pages/home.scss
@@ -1,3 +1,55 @@
+.camera-background {
+ background-size: 60px 47px;
+ width: 60px;
+ height: 47px;
+ background-image: url(asset-path("camera@2x.png"));
+ background-color: transparent;
+ border-style: none;
+}
+
+#icn-donation-jar {
+ margin-bottom: 30px;
+}
+
+.brave-rewards-banner {
+ &--background {
+ &-label {
+ display: block;
+ margin-left: -4px;
+ margin-top: 5px;
+ font-size: 16px;
+ }
+
+ &-camera {
+ margin-left: 46%;
+ margin-top: -60px;
+ }
+ }
+ &--logo {
+ &-parent {
+ position: relative;
+ }
+
+ &-label {
+ display: block;
+ margin-left: -4px;
+ margin-top: 5px;
+ font-size: 16px;
+ }
+
+ &-no-attachment {
+ margin-left: 30%;
+ position: absolute;
+ }
+
+ &-camera {
+ margin-left: 30%;
+ margin-top: 30%;
+ position: absolute;
+ }
+ }
+}
+
.nav {
display: block;
diff --git a/app/controllers/banners_controller.rb b/app/controllers/banners_controller.rb
new file mode 100644
index 0000000000..c55add06d2
--- /dev/null
+++ b/app/controllers/banners_controller.rb
@@ -0,0 +1,5 @@
+class BannersController < ApplicationController
+ def new
+ @publisher_id = current_publisher.id
+ end
+end
diff --git a/app/controllers/publishers/site_banners_controller.rb b/app/controllers/publishers/site_banners_controller.rb
new file mode 100644
index 0000000000..f3b87c7cc5
--- /dev/null
+++ b/app/controllers/publishers/site_banners_controller.rb
@@ -0,0 +1,105 @@
+class Publishers::SiteBannersController < ApplicationController
+ include ImageConversionHelper
+ before_action :authenticate_publisher!
+
+ MAX_IMAGE_SIZE = 10_000_000
+
+ def new
+ @site_banner = current_publisher.site_banner || SiteBanner.new
+ end
+
+ def create
+ site_banner = current_publisher.site_banner || SiteBanner.new
+ donation_amounts = JSON.parse(params[:donation_amounts])
+ site_banner.update(
+ publisher_id: current_publisher.id,
+ title: params[:title],
+ donation_amounts: donation_amounts,
+ default_donation: donation_amounts.second,
+ social_links: params[:social_links].present? ? JSON.parse(params[:social_links]) : {},
+ description: params[:description]
+ )
+ head :ok
+ end
+
+ def fetch
+ site_banner = current_publisher.site_banner
+ data = JSON.parse(site_banner.to_json)
+ data[:backgroundImage] = current_publisher.site_banner.read_only_react_property[:backgroundUrl]
+ data[:logoImage] = current_publisher.site_banner.read_only_react_property[:logoUrl]
+ render(json: data.to_json)
+ end
+
+ def update_logo
+ if params[:image].length > MAX_IMAGE_SIZE
+ # (Albert Wang): We should consider supporting alerts. This might require a UI redesign
+ # alert[:error] = "File size too big!"
+ head :payload_too_large and return
+ end
+ site_banner = current_publisher.site_banner
+ update_image(attachment: site_banner.logo, attachment_type: SiteBanner::LOGO)
+ head :ok
+ end
+
+ def update_background_image
+ if params[:image].length > MAX_IMAGE_SIZE
+ # (Albert Wang): We should consider supporting alerts. This might require a UI redesign
+ # alert[:error] = "File size too big!"
+ head :payload_too_large and return
+ end
+ site_banner = current_publisher.site_banner
+ update_image(attachment: site_banner.background_image, attachment_type: SiteBanner::BACKGROUND)
+ head :ok
+ end
+
+ private
+
+ def update_image(attachment:, attachment_type:)
+ data_url = params[:image].split(',')[0]
+ if data_url.starts_with?("data:image/jpeg")
+ content_type = "image/jpeg"
+ extension = ".jpg"
+ elsif data_url.starts_with?("data:image/png")
+ content_type = "image/png"
+ extension = ".png"
+ elsif data_url.starts_with?("data:image/bmp")
+ content_type = "image/bmp"
+ extension = ".bmp"
+ else
+ LogException.perform(StandardError.new("Unknown image format:" + data_url), params: {})
+ return nil
+ end
+ filename = Time.now.to_s.gsub!(" ", "_").gsub!(":", "_") + current_publisher.id
+
+ temp_file = Tempfile.new([filename, extension])
+ File.open(temp_file.path, 'wb') do |f|
+ f.write(Base64.decode64(params[:image].split(',')[1]))
+ end
+
+ original_image_path = temp_file.path
+
+ resized_jpg_path = resize_to_dimensions_and_convert_to_jpg(
+ source_image_path: original_image_path,
+ attachment_type: attachment_type,
+ filename: filename
+ )
+
+ begin
+ padded_resized_jpg_path = add_padding_to_image(
+ source_image_path: resized_jpg_path,
+ attachment_type: attachment_type,
+ )
+ rescue OutsidePaddingRangeError
+ logger.error "Outside padding range"
+ LogException.perform(StandardError.new("File size too big for #{attachment_type}"), params: {publisher_id: current_publisher.id})
+ end
+
+ new_filename = generate_filename(source_image_path: padded_resized_jpg_path)
+
+ attachment.attach(
+ io: open(padded_resized_jpg_path),
+ filename: new_filename + ".jpg",
+ content_type: "image/jpg"
+ )
+ end
+end
diff --git a/app/controllers/publishers_controller.rb b/app/controllers/publishers_controller.rb
index dc5825e68c..719a55cdf0 100644
--- a/app/controllers/publishers_controller.rb
+++ b/app/controllers/publishers_controller.rb
@@ -199,13 +199,13 @@ def update
end
respond_to do |format|
- format.json {
- if success
- head :no_content
- else
- render(json: { errors: publisher.errors }, status: 400)
- end
- }
+ if success
+ format.json { head :no_content }
+ format.html { redirect_to home_publishers_path }
+ else
+ format.json { render(json: { errors: publisher.errors }, status: 400) }
+ format.html { render(status: 400) }
+ end
end
end
diff --git a/app/helpers/image_conversion_helper.rb b/app/helpers/image_conversion_helper.rb
new file mode 100644
index 0000000000..4970317887
--- /dev/null
+++ b/app/helpers/image_conversion_helper.rb
@@ -0,0 +1,87 @@
+module ImageConversionHelper
+ IMAGE_QUALITY = 75
+
+ def resize_to_dimensions_and_convert_to_jpg(source_image_path:, attachment_type:, filename:)
+ # Set dimensions
+ mini_magick_image = MiniMagick::Image.open(source_image_path)
+ if attachment_type == SiteBanner::LOGO
+ mini_magick_image.resize(SiteBanner::LOGO_DIMENSIONS.join("x"))
+ elsif attachment_type == SiteBanner::BACKGROUND
+ mini_magick_image.resize(SiteBanner::BACKGROUND_DIMENSIONS.join("x"))
+ else
+ LogException.perform(StandardError.new("Unknown attachment type:" + attachment_type), params: {})
+ return nil
+ end
+
+ # Use JPG as its the most efficient standard
+ mini_magick_image.format "jpg"
+
+ new_filename = filename + "_resized"
+ temp_file = Tempfile.new([new_filename, ".jpg"])
+
+ mini_magick_image.write(temp_file)
+
+ temp_file.path
+ end
+
+=begin
+ Adding an empty comment adds an arbitrary number of bytes
+ Add a character to a comment adds 5 bytes (4 for padding)
+=end
+ def add_padding_to_image(source_image_path:, attachment_type:)
+ # Add initial conversion to get file size
+ if attachment_type == SiteBanner::LOGO
+ target_file_size = SiteBanner::LOGO_UNIVERSAL_FILE_SIZE
+ elsif attachment_type == SiteBanner::BACKGROUND
+ target_file_size = SiteBanner::BACKGROUND_UNIVERSAL_FILE_SIZE
+ else
+ LogException.perform(StandardError.new("Unknown attachment_type:" + attachment_type), params: {})
+ end
+
+ file_size_after_one_byte = calculate_image_size_after_pad_one_byte(source_image_path)
+
+ delta = target_file_size - file_size_after_one_byte
+
+ if delta < 0
+ raise OutsidePaddingRangeError
+ end
+
+ MiniMagick::Tool::Convert.new do |convert|
+ convert << source_image_path
+ convert.merge!(["-set", "comment", "a" * (delta + 1)])
+ convert.merge!(["-quality", IMAGE_QUALITY])
+ convert << source_image_path
+ end
+
+ source_image_path
+ end
+
+ def generate_filename(source_image_path:)
+ File.open(source_image_path, 'r') do |f|
+ Digest::SHA256.hexdigest f.read
+ end
+ end
+
+ private
+
+ def calculate_image_size_after_pad_one_byte(source_image_path)
+ mini_magick_image = MiniMagick::Image.open(source_image_path)
+
+ new_filename = "_resized"
+
+ temp_file = Tempfile.new([new_filename, ".jpg"])
+
+ mini_magick_image.write(temp_file)
+
+ MiniMagick::Tool::Convert.new do |convert|
+ convert << temp_file.path
+ convert.merge!(["-set", "comment", "a"])
+ convert.merge!(["-quality", IMAGE_QUALITY])
+ convert << temp_file.path
+ end
+
+ File.size(temp_file.path)
+ end
+
+ class OutsidePaddingRangeError < RuntimeError; end
+end
diff --git a/app/javascript/locale/en.js b/app/javascript/locale/en.js
new file mode 100644
index 0000000000..0bf7096aee
--- /dev/null
+++ b/app/javascript/locale/en.js
@@ -0,0 +1,10 @@
+const locale = {
+ about: 'about',
+ donationAmount: 'Donation amount',
+ makeMonthly: 'Make this monthly',
+ sendDonation: 'Send my donation',
+ tokens: 'tokens',
+ walletBalance: 'wallet balance',
+};
+
+export default locale;
diff --git a/app/javascript/packs/brave_rewards_banner.jsx b/app/javascript/packs/brave_rewards_banner.jsx
new file mode 100644
index 0000000000..da2718ded9
--- /dev/null
+++ b/app/javascript/packs/brave_rewards_banner.jsx
@@ -0,0 +1,443 @@
+import React from 'react'
+
+import BatsBackground from '../../assets/images/bg_bats.svg'
+import HeartsBackground from '../../assets/images/bg_hearts.svg'
+import { BatColorIcon, YoutubeColorIcon, TwitterColorIcon, TwitchColorIcon } from 'brave-ui/components/icons'
+import Checkbox from 'brave-ui/components/formControls/checkbox'
+
+import {styles} from '../packs/brave_rewards_banner.style.jsx'
+
+export default class BraveRewardsBanner extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ title: 'Your title',
+ description: 'Welcome to Brave Rewards banner',
+ backgroundImage: null,
+ backgroundImageData: '',
+ logoImage: null,
+ logoImageData: '',
+ youtube: '',
+ twitter: '',
+ twitch: '',
+ conversionRate: 0.2,
+ donationAmounts: [1, 5, 10],
+ mode: 'Edit',
+ width: '1320'
+ }
+ this.handleBackgroundImageUpload = this.handleBackgroundImageUpload.bind(this);
+ this.handleLogoImageUpload = this.handleLogoImageUpload.bind(this);
+ this.updateTitle = this.updateTitle.bind(this);
+ this.updateDescription = this.updateDescription.bind(this)
+ this.handleSave = this.handleSave.bind(this);
+ this.fetchSiteBanner = this.fetchSiteBanner.bind(this);
+ this.updateYoutube = this.updateYoutube.bind(this);
+ this.updateTwitter = this.updateTwitter.bind(this);
+ this.updateTwitch = this.updateTwitch.bind(this);
+ this.fetchSiteBanner = this.fetchSiteBanner.bind(this);
+ this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
+ }
+
+ componentWillMount(){
+ this.modalize();
+ }
+
+ componentDidMount(){
+ this.fetchSiteBanner();
+ document.getElementsByClassName('brave-rewards-banner-control-bar-save-button')[0].addEventListener("click", this.handleSave);
+ window.addEventListener('resize', this.updateWindowDimensions);
+ }
+
+ updateWindowDimensions() {
+ if(window.innerWidth < 991){
+ this.close();
+ }
+ else{
+ this.setState({ width: 1320});
+ }
+}
+
+ modalize(){
+ document.getElementsByClassName("modal-panel")[0].style.maxWidth = 'none';
+ document.getElementsByClassName("modal-panel")[0].style.padding = '0px';
+ document.getElementsByClassName("modal-panel--content")[0].style.padding = '0px';
+ }
+
+ close(){
+ document.getElementsByClassName("modal-panel")[0].style.maxWidth = '40rem';
+ document.getElementsByClassName("modal-panel")[0].style.padding = '2rem 2rem';
+ document.getElementsByClassName("modal-panel--content")[0].style.padding = '1rem 1rem 0 1rem';
+ document.getElementsByClassName("modal-panel--close js-deny")[0].click();
+ }
+
+ fetchSiteBanner(){
+ let that = this
+ let id = document.getElementById("publisher_id").value;
+ let url = '/publishers/' + id + "/site_banners/fetch";
+
+ fetch(url, {
+ method: 'GET',
+ headers: {
+ 'Accept': 'text/html',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-CSRF-Token': document.head.querySelector("[name=csrf-token]").content
+ },
+ credentials: "same-origin",
+ }).then(function(response) {
+ return response.json();
+ })
+ .then(function(banner) {
+
+ that.setState({
+ title: banner.title,
+ description: banner.description,
+ backgroundImage: banner.backgroundImage,
+ youtube: banner.social_links.youtube,
+ twitter: banner.social_links.twitter,
+ twitch: banner.social_links.twitch,
+ donationAmounts: banner.donation_amounts,
+ })
+
+ that.cropFetchedLogo(banner.logoImage, that);
+ });
+ }
+
+ updateTitle(event){
+ this.setState({title: event.target.value})
+ }
+
+ updateDescription(event){
+ this.setState({description: event.target.value})
+ }
+
+ updateYoutube(event){
+ this.setState({youtube: event.target.value})
+ }
+
+ updateTwitter(event){
+ this.setState({twitter: event.target.value})
+ }
+
+ updateTwitch(event){
+ this.setState({twitch: event.target.value})
+ }
+
+ updateDonationAmounts(event, index){
+ let temp = this.state.donationAmounts
+ if(/^(\s*|\d+)$/.test(event.target.value)){
+ temp[index] = event.target.value
+ this.setState({donationAmounts: temp})
+ }
+ }
+
+ handleBackgroundImageUpload(event) {
+ if (!event.target.files[0]) {
+ return
+ }
+ this.setState({backgroundImage: URL.createObjectURL(event.target.files[0]), backgroundImageData: event.target})
+ }
+
+ handleLogoImageUpload(event) {
+ let that = this;
+ this.cropLogo(event, that);
+ }
+
+ cropFetchedLogo(logo, that){
+ let img = new Image();
+ img.src = logo;
+ img.onload = function() {
+ var canvas = document.createElement('canvas');
+ var ctx = canvas.getContext('2d');
+ canvas.width = 160;
+ canvas.height = 160;
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+ let url = canvas.toDataURL('image/jpeg', 1);
+ that.setState({logoImage: url});
+ }
+ }
+
+ cropLogo(event, that){
+ if (event.target.files && event.target.files[0]) {
+ var filerdr = new FileReader();
+
+ filerdr.onload = function(e) {
+ var img160 = new Image();
+ var img480 = new Image();
+
+ img480.onload = function() {
+ var canvas = document.createElement('canvas');
+ var ctx = canvas.getContext('2d');
+ canvas.width = 480;
+ canvas.height = 480;
+ ctx.drawImage(img480, 0, 0, canvas.width, canvas.height);
+ canvas.toBlob(function(blob){
+ let file = {};
+ file["files"] = [blob]
+ that.setState({logoImageData: file});
+ });
+ }
+
+ img160.onload = function() {
+ var canvas = document.createElement('canvas');
+ var ctx = canvas.getContext('2d');
+ canvas.width = 160;
+ canvas.height = 160;
+ ctx.drawImage(img160, 0, 0, canvas.width, canvas.height);
+ let url = canvas.toDataURL('image/jpeg', 1);
+ that.setState({logoImage: url});
+ }
+
+ img160.src = e.target.result;
+ img480.src = e.target.result;
+ }
+ filerdr.readAsDataURL(event.target.files[0]);
+ }
+ }
+
+ handleSave(event) {
+ let that = this
+ let id = document.getElementById("publisher_id").value;
+ let url = '/publishers/' + id + "/site_banners";
+ let body = new FormData();
+
+ body.append('title', this.state.title);
+ body.append('description', this.state.description);
+ body.append('donation_amounts', JSON.stringify(this.state.donationAmounts));
+ body.append('social_links', JSON.stringify({youtube: this.state.youtube, twitter: this.state.twitter, twitch: this.state.twitch}));
+
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'text/html',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-CSRF-Token': document.head.querySelector("[name=csrf-token]").content
+ },
+ credentials: "same-origin",
+ body: body
+ }).then (
+ function(response) {
+ function submitById(id, suffix) {
+
+ const url = '/publishers/' + document.getElementById("publisher_id").value + "/site_banners/update_" + suffix;
+
+ let file;
+ if(suffix === 'background_image'){ file = that.state.backgroundImageData }
+ else if(suffix === 'logo'){ file = that.state.logoImageData }
+
+ if (file === "" || file === null) { return; }
+ var reader = new FileReader();
+ reader.readAsDataURL(file.files[0]);
+
+ reader.onloadend = function () {
+ const body = new FormData();
+ body.append('image', reader.result);
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'text/html',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-CSRF-Token': document.head.querySelector("[name=csrf-token]").content
+ },
+ credentials: "same-origin",
+ body: body
+ });
+ }
+ }
+
+ if (response.status === 200) {
+ submitById("background-image-select-input", "background_image");
+ submitById("logo-image-select-input", "logo");
+ }
+ }).then(
+ function(response) {
+ that.close();
+ });
+ }
+
+ render() {
+
+ let style = styles
+
+ let logoImg
+ let logoLabel
+ let backgroundImageLabel
+ let backgroundImg
+ let donationsInput
+ let explanatoryTitle
+ let explanatoryDescription
+ let socialLinkText
+
+ let rewardsBanner = {
+ maxWidth: this.state.width,
+ height:'488px',
+ overflow:'hidden'
+ }
+
+
+
+ if(this.props.mode === 'Edit'){
+ logoLabel = { height:'100%', width:'100%', borderRadius: '50%', border: '2px dotted white', cursor:'pointer'}
+ backgroundImageLabel = {height:'100%', width:'100%', border: '2px dotted white', cursor:'pointer'}
+
+ explanatoryTitle = {width:'100%', height:'50px', backgroundColor: 'rgba(0, 0, 0, 0)', border: '1px solid lightGray', borderRadius: '4px', marginTop: '15px', fontSize: '32px', color: '#686978'}
+ explanatoryDescription = {width:'100%', height:'150px', resize: 'none', backgroundColor: 'rgba(0, 0, 0, 0)', border:'1px solid lightGray', borderRadius: '4px', marginTop: '25px', fontSize: '22px', color: '#686978'}
+ donationsInput = {backgroundColor: 'rgba(0, 0, 0, 0)', marginRight:'5px', border: '1px solid lightGray', borderRadius: '4px', color: 'white', height:'19px'}
+ socialLinkText = {marginTop:'auto', marginBottom:'auto', borderBottom: '1px solid lightGray', borderTop: '1px solid rgba(0, 0, 0, 0)', borderLeft: '1px solid rgba(0, 0, 0, 0)', color: '#686978', width: '90px', fontSize: '15px', backgroundColor: 'rgba(0, 0, 0, 0)', borderRadius: '0px'}
+
+ if(this.state.backgroundImage === null){
+ backgroundImg = {height: '176px', padding: '10px', background: `url(${BatsBackground}) left top no-repeat, url(${HeartsBackground}) right top no-repeat, rgb(158, 159, 171)`}
+ }
+ else{
+ backgroundImg = {height: '176px', padding: '10px', background: `linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url(${this.state.backgroundImage})`}
+ }
+ if(this.state.logoImage === null){
+ logoImg = {position: 'absolute', top: '190px', left: '35px', borderRadius: '50%', width: '160px', height: '160px', border: '6px solid white', padding: '10px', backgroundColor:'#FB542B'}
+ }
+ else{
+ logoImg = {position: 'absolute', top: '190px', left: '35px', borderRadius: '50%', width: '160px', height: '160px', border: '6px solid white', padding: '10px', background:`url(${this.state.logoImage})`}
+ }
+ }
+ else{
+ logoLabel = { height:'100%', width:'100%', borderRadius: '50%', border: 'none', pointerEvents: 'none'}
+ backgroundImageLabel = {height:'100%', width:'100%', border: 'none', pointerEvents: 'none'}
+
+ explanatoryTitle = {width:'100%', height:'50px', backgroundColor: 'rgba(0, 0, 0, 0)', border: '1px solid rgba(0, 0, 0, 0)', borderRadius: '4px', marginTop: '15px', fontSize: '32px', color: '#686978', userSelect:'none'}
+ explanatoryDescription = {width:'100%', height:'150px', resize: 'none', backgroundColor: 'rgba(0, 0, 0, 0)', border:'1px solid rgba(0, 0, 0, 0)', borderRadius: '4px', marginTop: '25px', fontSize: '22px', color: '#686978', userSelect:'none'}
+ donationsInput = {backgroundColor: 'rgba(0, 0, 0, 0)', marginRight:'5px', border: '1px solid rgba(0, 0, 0, 0)', borderRadius: '4px', color: 'white'}
+ socialLinkText = {marginTop:'auto', marginBottom:'auto', borderBottom: '1px solid rgba(0, 0, 0, 0)', borderTop: '1px solid rgba(0, 0, 0, 0)', borderLeft: '1px solid rgba(0, 0, 0, 0)', color: '#686978', width: '90px', fontSize: '15px', backgroundColor: 'rgba(0, 0, 0, 0)', borderRadius: '0px', userSelect: 'none'}
+
+ if(this.state.backgroundImage === null){
+ backgroundImg = {height: '176px', padding: '10px', background: `url(${BatsBackground}) left top no-repeat, url(${HeartsBackground}) right top no-repeat, rgb(158, 159, 171)`}
+ }
+ else{
+ backgroundImg = {height: '176px', padding: '10px', background: `url(${this.state.backgroundImage})`}
+ }
+ if(this.state.logoImage === null){
+ logoImg = {position: 'absolute', top: '190px', left: '35px', borderRadius: '50%', width: '160px', height: '160px', border: '6px solid white', padding: '10px', backgroundColor:'#FB542B'}
+ }
+ else{
+ logoImg = {position: 'absolute', top: '190px', left: '35px', borderRadius: '50%', width: '160px', height: '160px', border: '6px solid white', padding: '10px', background:`url(${this.state.logoImage})`}
+ }
+ }
+
+ style.logoLabel = logoLabel
+ style.backgroundImageLabel = backgroundImageLabel
+ style.logoImg = logoImg
+ style.backgroundImg = backgroundImg
+ style.explanatoryTitle = explanatoryTitle
+ style.explanatoryDescription = explanatoryDescription
+ style.donationsInput = donationsInput
+ style.socialLinkText = socialLinkText
+ style.rewardsBanner = rewardsBanner
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Donation Amount
+
Wallet Balance: 25 BAT
+
+
+
+
+
+ {
+ this.props.mode === 'Edit'?
+ this.updateDonationAmounts(e, 0)} value={this.state.donationAmounts[0]} className="brave-rewards-banner-content-explanatory-text-headline" maxLength="3" size="3" type='text'/>:
+ (this.state.donationAmounts[0])
+ }
+ BAT
+
+
${(this.state.donationAmounts[0] * this.state.conversionRate).toFixed(2)} USD
+
+
+
+ {
+ this.props.mode === 'Edit'?
+ this.updateDonationAmounts(e, 1)} value={this.state.donationAmounts[1]} className="brave-rewards-banner-content-explanatory-text-headline" maxLength="3" size="3" type='text'/>:
+ (this.state.donationAmounts[1])
+ }
+ BAT
+
+
+
${(this.state.donationAmounts[1] * this.state.conversionRate).toFixed(2)} USD
+
+
+
+
+ {
+ this.props.mode === 'Edit'?
+ this.updateDonationAmounts(e, 2)} value={this.state.donationAmounts[2]} className="brave-rewards-banner-content-explanatory-text-headline" maxLength="3" size="3" type='text'/>:
+ (this.state.donationAmounts[2])
+ }
+ BAT
+
+
+
${(this.state.donationAmounts[2] * this.state.conversionRate).toFixed(2)} USD
+
+
+
+
+ Make this monthly
+
+
+
+
+
+
+
+
+
+
+
+ CURRENTLY DONATING BAT MONTHLY
+
+
+
+ SEND MY DONATION
+
+
+
+
+ );
+ }
+}
diff --git a/app/javascript/packs/brave_rewards_banner.style.jsx b/app/javascript/packs/brave_rewards_banner.style.jsx
new file mode 100644
index 0000000000..47b9a583fb
--- /dev/null
+++ b/app/javascript/packs/brave_rewards_banner.style.jsx
@@ -0,0 +1,120 @@
+
+ let bannerContent = {
+ display: 'grid',
+ gridTemplateColumns: '3fr 5fr 4fr',
+ height: '262px'
+ }
+
+ let donationButton = {
+ width: '100px',
+ textAlign: 'center',
+ borderRadius: '20px',
+ padding: '11px 10px',
+ fontSize: '13px',
+ backgroundColor: '#AAAFEF',
+ color: 'white',
+ float: 'left'
+ }
+
+ let socialLinks = {
+ backgroundColor: 'rgb(233, 240, 255)',
+ paddingTop: '100px',
+ paddingLeft:'40px'
+ }
+
+ let socialLink = {
+ display: 'grid',
+ gridTemplateColumns: '1fr 3fr',
+ fontSize:'14px',
+ height: '25px',
+ marginTop: '20px',
+ marginBottom: '20px'
+ }
+
+ let imageInput = {
+ opacity:'0',
+ position:'absolute',
+ }
+
+ let explanatoryText = {
+ backgroundColor: 'rgb(233, 240, 255)',
+ paddingRight:'30px'
+ }
+
+ let donations = {
+ backgroundColor: 'rgb(105, 111, 220)',
+ color:'white'
+ }
+
+ let batIcon = {
+ position:'absolute',
+ height:'40px',
+ width:'40px',
+ right:'0',
+ marginTop:'15px',
+ marginRight:'40px'
+ }
+
+ let bottomBar = {
+ display: 'grid',
+ gridTemplateColumns: '8fr 4fr',
+ height: '50px'
+ }
+
+ let donationsCurrent = {
+ height:'50px',
+ backgroundColor:'#E9E9F4',
+ justifyContent:'center',
+ fontSize:'13px',
+ fontWeight:'600',
+ color:'grey',
+ textAlign:'center',
+ paddingTop:'auto',
+ paddingBottom:'auto',
+ display:'flex',
+ alignItems:'center'
+ }
+
+ let donationsSend = {
+ height:'50px',
+ backgroundColor:'#4C54D2',
+ justifyContent:'center',
+ fontSize:'13px',
+ fontWeight:'600',
+ color:'white',
+ textAlign:'center',
+ paddingTop:'auto',
+ paddingBottom:'auto',
+ display:'flex',
+ alignItems:'center'
+ }
+
+ let donationsConverted = {
+ fontSize:'13px',
+ float:'right',
+ paddingTop:'11px',
+ marginRight:'40px'
+ }
+
+ let donationsButtonContainer = {
+ height:'42px',
+ marginTop:'10px',
+ marginBottom:'10px'
+ }
+
+
+export let styles = {
+ bannerContent: bannerContent,
+ donationButton: donationButton,
+ socialLinks: socialLinks,
+ socialLink: socialLink,
+ imageInput: imageInput,
+ explanatoryText: explanatoryText,
+ donations: donations,
+ batIcon: batIcon,
+ bottomBar: bottomBar,
+ donationsCurrent: donationsCurrent,
+ donationsSend: donationsSend,
+ donationsConverted: donationsConverted,
+ donationsButtonContainer: donationsButtonContainer
+}
diff --git a/app/javascript/packs/brave_rewards_banner_container.jsx b/app/javascript/packs/brave_rewards_banner_container.jsx
new file mode 100644
index 0000000000..c67794cdfe
--- /dev/null
+++ b/app/javascript/packs/brave_rewards_banner_container.jsx
@@ -0,0 +1,130 @@
+import React from 'react'
+import ReactDOM from 'react-dom'
+
+import BraveRewardsBannerControlBar from '../packs/brave_rewards_banner_control_bar.jsx'
+import BraveRewardsBannerIntro from '../packs/brave_rewards_banner_intro.jsx'
+import BraveRewardsBanner from '../packs/brave_rewards_banner.jsx'
+
+import BraveRewardsLogo from '../../assets/images/icn-donation-jar@1x.png'
+
+
+export default class BraveRewardsBannerContainer extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ mode: 'Edit',
+ isIntro: false,
+ }
+
+ this.setMode = this.setMode.bind(this)
+ }
+
+ componentWillMount(){
+ this.modalize();
+ }
+
+ modalize(){
+ document.getElementsByClassName("modal-panel")[0].style.maxWidth = 'none';
+ document.getElementsByClassName("modal-panel")[0].style.padding = '0px';
+ document.getElementsByClassName("modal-panel--content")[0].style.padding = '0px';
+ }
+
+ setMode(mode){
+ this.setState({mode: mode})
+ }
+
+ render() {
+
+ let controlButton = {
+ width: '200px',
+ textAlign: 'center',
+ borderRadius: '24px',
+ padding: '14px 15px',
+ fontSize: '14px',
+ marginLeft:'20px',
+ border: '1px solid #fc4145',
+ color: '#fc4145',
+ cursor: 'pointer',
+ userSelect: 'none'
+ }
+
+ let introButton = {
+ width: '200px',
+ textAlign: 'center',
+ borderRadius: '24px',
+ padding: '14px 15px',
+ fontSize: '14px',
+ margin: 'auto',
+ marginTop: '20px',
+ border: '1px solid #fc4145',
+ color: '#fc4145',
+ cursor: 'pointer',
+ userSelect: 'none'
+ }
+
+ let rewardsBannerContainer
+
+ if(this.state.isIntro){
+ rewardsBannerContainer = {height:'590px', width:'840px', display: 'flex', flexDirection: 'column', justifyContent: 'center'}
+ }
+ else{
+ rewardsBannerContainer = {height:'590px', width:'840px'}
+ }
+
+ let modeButton;
+
+ if (this.state.mode === 'Edit') {
+ modeButton = this.setMode('Preview')} className="brave-rewards-banner-control-bar-save-button" id="edit-button" style={controlButton}>Preview banner
;
+ } else {
+ modeButton = this.setMode('Edit')} className="brave-rewards-banner-control-bar-save-button" id="preview-button" style={controlButton}>Edit banner
;
+ }
+
+ return (
+
+
+ {
+ this.state.isIntro ?
+ (
+
+
+ {this.props.headline}
+
+
+ {this.props.intro}
+
+
+
+
+
this.setState({isIntro:false})} style={introButton}>Begin
+
) :
+ (
+
+
+
Save change
+ {modeButton}
+
+
+
+ )
+ }
+
+
+ );
+ }
+}
+
+export function renderBraveRewardsBannerContainer(headline, intro) {
+
+ ReactDOM.render(
+ ,
+ document.getElementById("rewards-banner-container").parentElement.parentElement
+ )
+}
+
+export function unmountBraveRewardsBannerContainer() {
+ ReactDOM.unmountComponentAtNode(
+ ,
+ document.getElementsByClassName("react")[0]
+ )
+}
diff --git a/app/javascript/packs/brave_rewards_banner_control_bar.jsx b/app/javascript/packs/brave_rewards_banner_control_bar.jsx
new file mode 100644
index 0000000000..51383327d0
--- /dev/null
+++ b/app/javascript/packs/brave_rewards_banner_control_bar.jsx
@@ -0,0 +1,15 @@
+import React from 'react'
+
+export default class BraveRewardsBannerControlBar extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/javascript/packs/brave_rewards_banner_display.jsx b/app/javascript/packs/brave_rewards_banner_display.jsx
new file mode 100644
index 0000000000..9519c2acf7
--- /dev/null
+++ b/app/javascript/packs/brave_rewards_banner_display.jsx
@@ -0,0 +1,434 @@
+// Run this example by adding <%= javascript_pack_tag 'hello_react' %> to the head of your layout file,
+// like app/views/layouts/application.html.erb. All it does is render ButtonPrimary
at the bottom
+// of the page.
+
+import React from 'react'
+import ReactDOM from 'react-dom'
+import PropTypes from 'prop-types'
+import SiteBanner from 'brave-ui/features/rewards/siteBanner'
+import { initLocale } from 'brave-ui'
+import locale from 'locale/en'
+
+class BraveRewardsPageForm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ title: props.details.title || 'Your Title',
+ description: props.details.description || 'A brief description',
+ backgroundImage: props.details.backgroundUrl,
+ logo: props.details.logoUrl,
+ donationAmounts: props.details.donationAmounts || [1, 5, 10],
+ socialLinks: props.details.socialLinks || {'twitter': '@', 'youtube': '@', 'twitch': '@'},
+ };
+
+ this.updateTitle = this.updateTitle.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.updateTwitch = this.updateTwitch.bind(this);
+ this.updateYoutube = this.updateYoutube.bind(this);
+ this.updateTwitter = this.updateTwitter.bind(this);
+ this.handlePreviewButton = this.handlePreviewButton.bind(this);
+ this.handleLogoImageChange = this.handleLogoImageChange.bind(this);
+ this.handleBackgroundImageChange = this.handleBackgroundImageChange.bind(this);
+ this.handleDonationAmountsChange = this.handleDonationAmountsChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ isNormalInteger(str) {
+ return /^\+?(0|[1-9]\d*)$/.test(str);
+ }
+
+ convertDonationAmounts(donationAmounts) {
+ if (donationAmounts == null) return null;
+
+ return donationAmounts.map(
+ amount => ({
+ 'tokens': amount,
+ 'converted': this.props.conversionRate * amount,
+ 'selected': false
+ })
+ );
+ }
+
+ handlePreviewButton(){
+ ReactDOM.unmountComponentAtNode(document.getElementsByClassName("modal-panel--content")[0]);
+ document.getElementsByClassName("modal-panel--close")[0].click();
+ document.getElementById("instant-donation-button").click();
+ document.getElementById("preview-banner-button").click();
+ }
+
+ handleDonationAmountsChange(event) {
+ this.setState({
+ donationAmounts:
+ document.getElementById("donation-amounts-input").value.split(',').map(Number)
+ });
+ }
+
+ handleLogoImageChange(event) {
+ this.setState({logo: URL.createObjectURL(event.target.files[0])});
+ var logoImageDiv = document.getElementsByClassName("brave-rewards-banner--logo-no-attachment")[0];
+ var logoDiv = document.getElementsByClassName("sc-dnqmqq")[0];
+
+ if (this.state.logo != null) {
+ logoImageDiv.classList.remove("brave-rewards-banner--logo-no-attachment");
+ logoImageDiv.classList.add("brave-rewards-banner--logo-camera");
+ logoDiv.classList.add("brave-rewards-banner--logo-parent");
+ }
+ }
+
+ applyFade() {
+ if(this.props.editMode){
+ let bgClass = document.getElementsByClassName('sc-EHOje')[0].classList[1];
+ let bgElement = document.getElementsByClassName('sc-EHOje ' + bgClass)[0];
+ let bgStyle = getComputedStyle(bgElement);
+ let bgUrl = bgStyle.background.match(/url\(([^)]+)\)/i)[1];
+ let bgFaded = 'linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), url(' + this.state.backgroundImage + ')';
+
+ //Check for default image
+ if(!bgUrl.includes('/rewards/siteBanner/assets/')){
+ bgElement.style.backgroundImage = bgFaded
+ }
+ }
+ }
+
+ handleBackgroundImageChange(event) {
+ this.setState({backgroundImage: URL.createObjectURL(event.target.files[0])});
+ this.applyFade();
+ // (Albert Wang): TODO: We'll revisit this later
+ }
+
+ setupBackgroundLabel() {
+ // Allow uploads
+ var backgroundButton = document.createElement("button");
+ backgroundButton.id = "background-image-select-button";
+ backgroundButton.classList.add("camera-background");
+
+ var backgroundInput = document.createElement("input");
+ backgroundInput.id="background-image-select-input";
+ backgroundInput.type="file";
+ backgroundInput.style.display = "none";
+ backgroundInput.onchange = this.handleBackgroundImageChange;
+
+ backgroundButton.addEventListener("click", function (e) {
+ if (backgroundInput) {
+ backgroundInput.click();
+ }
+ }, false);
+
+ var label = document.createElement("label");
+ label.innerHTML="900 x 176";
+ label.classList.add("brave-rewards-banner--background-label");
+
+ var backgroundDiv = document.getElementsByClassName("sc-EHOje")[0];
+ var callToActionDiv = document.createElement("div");
+ callToActionDiv.classList.add("brave-rewards-banner--background-camera");
+ backgroundDiv.append(callToActionDiv);
+ callToActionDiv.appendChild(backgroundInput);
+ callToActionDiv.appendChild(backgroundButton);
+ callToActionDiv.appendChild(label);
+ }
+
+ setupLogoLabel() {
+ // Allow uploads
+ var logoButton = document.createElement("button");
+ logoButton.id = "logo-image-select-button";
+ logoButton.classList.add("camera-background");
+
+ var logoInput = document.createElement("input");
+ logoInput.id="logo-image-select-input";
+ logoInput.type="file";
+ logoInput.style.display = "none";
+ logoInput.onchange = this.handleLogoImageChange;
+
+ logoButton.addEventListener("click", function (e) {
+ if (logoInput) {
+ logoInput.click();
+ }
+ }, false);
+
+ var label = document.createElement("label");
+ label.innerHTML="148 x 148";
+ label.classList.add("brave-rewards-banner--logo-label");
+
+ var logoDiv = document.getElementsByClassName("sc-dnqmqq")[0];
+ logoDiv.classList.add("brave-rewards-banner--logo-parent");
+ var callToActionDiv = document.createElement("div");
+ if (this.state.logo == null) {
+ callToActionDiv.classList.add("brave-rewards-banner--logo-no-attachment");
+ } else {
+ callToActionDiv.classList.add("brave-rewards-banner--logo-camera");
+ }
+ logoDiv.prepend(callToActionDiv);
+ callToActionDiv.appendChild(logoInput);
+ callToActionDiv.appendChild(logoButton);
+ callToActionDiv.appendChild(label);
+ }
+
+ componentDidMount() {
+
+ this.applyFade();
+ document.getElementsByClassName("sc-gZMcBi")[0].remove();
+
+ if (this.props.editMode) {
+ this.setupBackgroundLabel();
+ this.setupLogoLabel();
+
+ // Set h3 editable
+
+
+ // Set p editable
+ // document.getElementsByClassName("sc-gqjmRU")[0].setAttribute("contenteditable", true)
+
+ var hiddenDonationAmounts = document.createElement('input');
+ hiddenDonationAmounts.id = 'donation-amounts-input';
+ hiddenDonationAmounts.type = "hidden"
+ hiddenDonationAmounts.style.display = 'none';
+ hiddenDonationAmounts.onchange = this.handleDonationAmountsChange;
+ document.body.appendChild(hiddenDonationAmounts);
+
+ // Editable for tokens
+ for (let element of document.getElementsByClassName("sc-brqgnP")) {
+ element.setAttribute("contenteditable", true);
+ var observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(mutation) {
+ if (mutation.type == "contentList") {
+ return;
+ }
+ // TODO: (Albert Wang) Make sure the input are valid numbers
+ var donationAmounts = [];
+ for (let amountSpan of document.getElementsByClassName("sc-brqgnP")) {
+ donationAmounts.push(parseInt(amountSpan.textContent));
+ }
+ document.getElementById("donation-amounts-input").value = donationAmounts;
+ document.getElementById("donation-amounts-input").onchange();
+ });
+ });
+ // configuration of the observer:
+ var config = { characterData: true, attributes: false, childList: true, subtree: true };
+ // pass in the target node, as well as the observer options
+ observer.observe(element, config);
+ };
+
+ // Hide X-mark
+ document.getElementsByClassName("sc-bZQynM")[0].style = "display: none";
+ }
+ }
+
+ submitById(id, suffix) {
+ const url = '/publishers/' + this.props.publisher_id + "/site_banners/update_" + suffix;
+ var file = document.getElementById(id);
+ var reader = new FileReader();
+
+ // Don't upload if user didn't upload a new image
+ if (file.value == "" || file.value == null) {
+ return;
+ }
+ reader.readAsDataURL(file.files[0]);
+ reader.onloadend = function () {
+ const body = new FormData();
+ body.append('image', reader.result);
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'text/html',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-CSRF-Token': document.head.querySelector("[name=csrf-token]").content
+ },
+ credentials: "same-origin",
+ body: body
+ });
+ };
+ }
+
+ updateDescription(event) {
+ this.setState({description: event.target.value})
+ }
+
+ updateTitle(event) {
+ this.setState({title: event.target.value})
+ }
+
+ updateTwitch(event) {
+ let temp = this.state.socialLinks
+ temp.twitch = event.target.value
+ this.setState({socialLink : temp});
+ }
+
+ updateYoutube(event) {
+ let temp = this.state.socialLinks
+ temp.youtube = event.target.value
+ this.setState({socialLink : temp});
+ }
+
+ updateTwitter(event) {
+ let temp = this.state.socialLinks
+ temp.twitter = event.target.value
+ this.setState({socialLink : temp});
+ }
+
+ handleSubmit(event) {
+ const url = '/publishers/' + this.props.publisher_id + "/site_banners";
+ const body = new FormData();
+
+ body.append('title', this.state.title);
+ body.append('description', this.state.description);
+ body.append('donation_amounts', JSON.stringify(this.state.donationAmounts));
+ body.append('social_links', JSON.stringify(this.state.socialLinks));
+
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'text/html',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-CSRF-Token': document.head.querySelector("[name=csrf-token]").content
+ },
+ credentials: "same-origin",
+ body: body
+ }).then (
+ function(response) {
+ function submitById(id, suffix) {
+ const url = '/publishers/' + document.getElementById("publisher_id").value + "/site_banners/update_" + suffix;
+ var file = document.getElementById(id);
+ var reader = new FileReader();
+
+ // Don't upload if user didn't upload a new image
+ if (file.value == "" || file.value == null) {
+ return;
+ }
+ reader.readAsDataURL(file.files[0]);
+ reader.onloadend = function () {
+ const body = new FormData();
+ body.append('image', reader.result);
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'text/html',
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-CSRF-Token': document.head.querySelector("[name=csrf-token]").content
+ },
+ credentials: "same-origin",
+ body: body
+ });
+ };
+ }
+ if (response.status === 200) {
+ submitById("background-image-select-input", "background_image");
+ submitById("logo-image-select-input", "logo");
+ }
+ // TODO: (Albert Wang): Make sure the above code doesn't reach here until a response is received
+ }).then(
+ function(response) {
+ ReactDOM.unmountComponentAtNode(document.getElementsByClassName("modal-panel--content")[0]);
+ document.getElementsByClassName("modal-panel--close")[0].click();
+ });
+ }
+
+ render() {
+ initLocale(locale);
+
+ let topController;
+
+ if (this.props.editMode) {
+ topController =
+
+ Don't Change
+ Preview Banner
+ Save Change
+
+ } else {
+ topController = Close
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {
+ // this.state.socialLinks.twitter !== undefined &&
+
+ }
+ {
+ // this.state.socialLinks.youtube !== undefined &&
+
+ }
+ {
+ // this.state.socialLinks.twitch !== undefined &&
+
+ }
+
+
+
+
+ Select a background image
+
+
+ );
+ }
+}
+
+export function renderBraveRewardsBannerDisplay(editMode) {
+ const braveRewardsPageForm = ;
+
+ ReactDOM.render(
+ braveRewardsPageForm,
+ document.getElementById("instant-donation-modal").parentElement.parentElement
+ )
+ document.getElementById('site_banner').children[1].style.height = '0vh';
+
+ // Resize modal container
+ document.getElementsByClassName("modal-container")[0].style.height = document.getElementById('site_banner').children[1].children[0].offsetHeight + "px";
+ document.getElementsByClassName("modal-container")[0].style.width = document.getElementById('site_banner').children[1].children[0].offsetWidth + "px";
+
+ // Reset margins
+ document.getElementsByClassName("modal-panel")[0].style.marginLeft = "0px";
+ document.getElementById("controller-form").style.marginLeft = "-48px";
+ document.getElementById("controller-form").style.marginTop = "-128px";
+ document.getElementById("controller-form").style.paddingTop = "16px";
+ document.getElementById("controller-form").style.width = document.getElementById('site_banner').children[1].children[0].offsetWidth + "px";
+ document.getElementById("controller-form").style.backgroundColor = "white";
+
+ // Hide unused close button
+ document.getElementsByClassName("modal-panel--close")[0].style.visibility = 'hidden';
+
+ document.getElementById("instant-donation-dont-save-changes").onclick = function() {
+ ReactDOM.unmountComponentAtNode(document.getElementsByClassName("modal-panel--content")[0]);
+ document.getElementsByClassName("modal-panel--close")[0].click();
+ }
+}
diff --git a/app/javascript/packs/brave_rewards_banner_intro.jsx b/app/javascript/packs/brave_rewards_banner_intro.jsx
new file mode 100644
index 0000000000..7e2d3ca995
--- /dev/null
+++ b/app/javascript/packs/brave_rewards_banner_intro.jsx
@@ -0,0 +1,15 @@
+import React from 'react'
+
+export default class BraveRewardsBannerIntro extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/app/javascript/publishers/home.js b/app/javascript/publishers/home.js
index f7af6b2af1..857cf2bdff 100644
--- a/app/javascript/publishers/home.js
+++ b/app/javascript/publishers/home.js
@@ -6,6 +6,7 @@ import fetch from '../utils/fetchPolyfill';
import flash from '../utils/flash';
import { Wallet } from '../wallet';
import { formatFullDate } from '../utils/dates';
+import { renderBraveRewardsBannerContainer } from '../packs/brave_rewards_banner_container';
// ToDo - import resource strings
const NO_CURRENCY_SELECTED = 'None selected';
@@ -376,11 +377,13 @@ document.addEventListener('DOMContentLoaded', function() {
if (e === event.target || e.nextSibling == event.target || e.nextSibling.firstChild == event.target) {
continue;
} else {
- hideVerificationFailureWhatHappened(e);
+ hideVerificationFailureWhatHappened(e);
}
}
})
+ let instantDonationButton = document.getElementById("instant-donation-button");
+
editContact.addEventListener('click', function(event) {
updateContactName.value = showContactName.innerText;
updateContactEmail.value = pendingContactEmail.innerText || showContactEmail.innerText;
@@ -399,6 +402,23 @@ document.addEventListener('DOMContentLoaded', function() {
event.preventDefault();
}, false);
+ instantDonationButton.addEventListener("click", function(event) {
+
+ document.getElementById("intro-container").style.padding = '50px';
+ document.getElementsByClassName("modal-panel")[0].style.padding = '0px';
+ document.getElementsByClassName("modal-panel--content")[0].style.padding = '0px';
+
+ document.getElementById("open-banner-button").onclick = function() {
+ renderBraveRewardsBannerContainer();
+ };
+
+ document.getElementsByClassName("modal-panel--close js-deny")[0].onclick = function(e) {
+ document.getElementsByClassName("modal-panel")[0].style.maxWidth = '40rem';
+ document.getElementsByClassName("modal-panel")[0].style.padding = '2rem 2rem';
+ document.getElementsByClassName("modal-panel--content")[0].style.padding = '1rem 1rem 0 1rem';
+ };
+ }, false);
+
updateContactForm.addEventListener('submit', function(event) {
event.preventDefault();
submitForm('update_contact', 'PATCH', true)
diff --git a/app/models/json_builders/channels_json_builder.rb b/app/models/json_builders/channels_json_builder.rb
index b8f2e2759e..1a1ca006ef 100644
--- a/app/models/json_builders/channels_json_builder.rb
+++ b/app/models/json_builders/channels_json_builder.rb
@@ -1,13 +1,18 @@
# Builds a list of distinct channels that are either verified, excluded or both for the Brave Browser.
-#
+#
# Each channel is an array:
-# [channel_identifier, verified, excluded]
+# [
+# channel_identifier (string),
+# verified (boolean),
+# excluded (boolean),
+# site_banner details
+# ]
#
# ex.
# [
-# ["brave.com", true, false],
-# ["google.com", false, true],
-# ["us.gov", false, false]
+# ["brave.com", true, false, {title: 'Hello', description: 'world'...}],
+# ["google.com", false, true, {}],
+# ["us.gov", false, false, {}]
# ]
class JsonBuilders::ChannelsJsonBuilder
@@ -21,10 +26,7 @@ def initialize
def build
channels = []
- [ Channel.verified.site_channels,
- Channel.youtube_channels,
- Channel.twitch_channels,
- Channel.twitter_channels].each do |verified_channels|
+ [Channel.left_joins(publisher: :site_banner).verified.site_channels, Channel.left_joins(publisher: :site_banner).youtube_channels, Channel.left_joins(publisher: :site_banner).twitch_channels, Channel.left_joins(publisher: :site_banner).twitter_channels].each do |verified_channels|
verified_channels.find_each do |verified_channel|
if @excluded_channel_ids.include?(verified_channel.details.channel_identifier)
excluded = true
@@ -32,15 +34,15 @@ def build
else
excluded = false
end
- channels.push([verified_channel.details.channel_identifier, true, excluded])
+ channels.push([verified_channel.details.channel_identifier, true, excluded, verified_channel.publisher&.site_banner&.read_only_react_property || {}])
end
end
@excluded_channel_ids.each do |excluded_channel_id|
next if @excluded_verified_channel_ids.include?(excluded_channel_id)
- channels.push([excluded_channel_id, false, true])
+ channels.push([excluded_channel_id, false, true, {}])
end
channels.to_json
end
-end
\ No newline at end of file
+end
diff --git a/app/models/publisher.rb b/app/models/publisher.rb
index 9643f8090f..38fc1df06a 100644
--- a/app/models/publisher.rb
+++ b/app/models/publisher.rb
@@ -18,6 +18,7 @@ class Publisher < ApplicationRecord
has_many :login_activities
has_many :channels, validate: true, autosave: true
+ has_one :site_banner
has_many :site_channel_details, through: :channels, source: :details, source_type: 'SiteChannelDetails'
has_many :youtube_channel_details, through: :channels, source: :details, source_type: 'YoutubeChannelDetails'
has_many :status_updates, -> { order(created_at: :desc) }, class_name: 'PublisherStatusUpdate'
@@ -272,6 +273,11 @@ def can_create_uphold_cards?
!excluded_from_payout
end
+ # (Albert Wang) We can remove this when beta is done
+ def in_brave_rewards_whitelist?
+ self.email.in?((Rails.application.secrets[:brave_rewards_email_whitelist] || "").split(","))
+ end
+
private
def set_created_status
diff --git a/app/models/site_banner.rb b/app/models/site_banner.rb
new file mode 100644
index 0000000000..3e58403846
--- /dev/null
+++ b/app/models/site_banner.rb
@@ -0,0 +1,39 @@
+require 'rubygems'
+require 'json'
+
+class SiteBanner < ApplicationRecord
+ include Rails.application.routes.url_helpers
+ has_one_attached :logo
+ has_one_attached :background_image
+ belongs_to :publisher
+
+ LOGO = "logo".freeze
+ LOGO_DIMENSIONS = [480,480]
+ LOGO_UNIVERSAL_FILE_SIZE = 30_000 # In bytes
+
+ BACKGROUND = "background".freeze
+ BACKGROUND_DIMENSIONS = [900,176]
+ BACKGROUND_UNIVERSAL_FILE_SIZE = 60_000 # In bytes
+
+ def read_only_react_property
+ {
+ title: self.title,
+ description: self.description,
+ backgroundUrl: url_for(self.background_image),
+ logoUrl: url_for(self.logo),
+ donationAmounts: self.donation_amounts,
+ socialLinks: self.social_links
+ }
+ end
+
+ def url_for(object)
+ return nil if object.nil? || object.attachment.nil?
+
+ if Rails.env.development? || Rails.env.test?
+ # (Albert Wang): I couldn't figure out how to play nicely with localhost
+ "https://localhost:3000" + rails_blob_path(object, only_path: true)
+ else
+ "#{Rails.application.secrets[:s3_rewards_public_domain]}/#{object.blob.key}"
+ end
+ end
+end
diff --git a/app/views/application/_nav.html.slim b/app/views/application/_nav.html.slim
index 9bdd0ae53e..216f611c4c 100644
--- a/app/views/application/_nav.html.slim
+++ b/app/views/application/_nav.html.slim
@@ -1,6 +1,10 @@
nav.navbar.navbar-default.navbar-static-top.top-nav-collapse
.container-fluid
.menu-container
+ .nav.pull-left.float-left
+ = link_to(root_path) do
+ .brave-logo
+ = render "logo_wordmark_svg"
.nav.pull-right.float-right
= yield(:navbar_content_right)
- if current_publisher
@@ -32,9 +36,3 @@ nav.navbar.navbar-default.navbar-static-top.top-nav-collapse
log_out_publishers_path, \
class: 'btn btn-secondary btn-highlight btn-outline-primary user-solo-button' \
)
- .nav.pull-left.float-left
- = link_to(root_path) do
- .brave-logo
- = render "logo_wordmark_svg"
- .text-center
- = yield(:navbar_content)
diff --git a/app/views/publishers/_instant_donation_modal.html.slim b/app/views/publishers/_instant_donation_modal.html.slim
new file mode 100644
index 0000000000..345a33a449
--- /dev/null
+++ b/app/views/publishers/_instant_donation_modal.html.slim
@@ -0,0 +1,11 @@
+.modal id="rewards_banner_intro_modal" role="dialog" tabindex="-1"
+ .modal-dialog
+ .modal-header id="instant-donation-modal-selection"
+ center id="intro-container"
+ = image_tag "icn-donation-jar@1x.png", id: 'icn-donation-jar'
+ h2.modal-title = t ".headline"
+ p = t ".intro"
+ = link_to(t(".continue"), "#", class: 'btn btn-primary', id: "open-banner-button")
+ center
+ .modal-header style="display: none;" id="rewards-banner-container"
+ = hidden_field_tag 'publisher_id', current_publisher.id
diff --git a/app/views/publishers/home.html.slim b/app/views/publishers/home.html.slim
index 70c8debfc5..c5f39c2d96 100644
--- a/app/views/publishers/home.html.slim
+++ b/app/views/publishers/home.html.slim
@@ -5,7 +5,6 @@ noscript
current_publisher.uphold_verified? && \
current_publisher.wallet.present? && current_publisher.wallet.authorized? && \
(current_publisher.default_currency_confirmed_at.nil? || !current_publisher.default_currency.present?)
-
script type="text/html" id="confirm_default_currency_modal_wrapper"
= render :partial => 'confirm_default_currency_modal'
@@ -16,6 +15,14 @@ script type="text/html" id="confirm_default_currency_modal_wrapper"
class: 'title' \
)
= render partial: "choose_channel_button"
+ - if current_publisher.in_brave_rewards_whitelist?
+ = link_to("+ #{t "shared.instant_donation"}", "#", data: { "js-confirm-with-modal": "instant-donation-modal" }, class: 'btn btn-secondary btn-highlight', id: 'instant-donation-button')
+
+
+= hidden_field_tag 'publisher_id', current_publisher.id
+= hidden_field_tag 'site-banner-react-props', (current_publisher&.site_banner&.read_only_react_property || {}).to_json
+/ TODO: (Albert Wang) Add conversion rate
+= hidden_field_tag 'conversion-rate', 0.3
.dashboard
#uphold_status { class=uphold_status_class(current_publisher)
@@ -206,7 +213,7 @@ script type="text/html" id="confirm_default_currency_modal_wrapper"
h4.bat-channel--amount id=("channel_amount_bat_#{channel.details.channel_identifier}")
- if channel.details_type == "TwitchChannelDetails"
= publisher_channel_balance(current_publisher, channel.details.author_identifier, "BAT")
- - else
+ - else
= publisher_channel_balance(current_publisher, channel.details.channel_identifier, "BAT")
span.bat-channel--currency= " BAT"
@@ -352,3 +359,6 @@ javascript:
document.addEventListener('DOMContentLoaded', function() {
javascriptDetected();
});
+
+script id="instant-donation-modal" type="text/html"
+ = render "instant_donation_modal"
diff --git a/app/views/publishers/site_banners/new.html.slim b/app/views/publishers/site_banners/new.html.slim
new file mode 100644
index 0000000000..80383e14ab
--- /dev/null
+++ b/app/views/publishers/site_banners/new.html.slim
@@ -0,0 +1,2 @@
+= javascript_pack_tag("brave_rewards_banner_display")
+= hidden_field_tag 'publisher_id', current_publisher.id
diff --git a/base.yml b/base.yml
index 7dbd614ef3..a790436e9a 100644
--- a/base.yml
+++ b/base.yml
@@ -24,6 +24,7 @@ services:
- BAT_REDDIT_URL
- BAT_ROCKETCHAT_URL
- BAT_TWITTER_URL
+ - BRAVE_REWARDS_EMAIL_WHITELIST
- FEE_RATE
- DATABASE_URL
- DATABASE_URL_TEST
@@ -65,4 +66,4 @@ services:
- UPHOLD_DASHBOARD_URL
- UPHOLD_SCOPE
- URL_HOST
- - YOUTUBE_API_KEY
\ No newline at end of file
+ - YOUTUBE_API_KEY
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 0d7b06a5c3..163a4ad5b2 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -43,6 +43,9 @@
address: Rails.application.secrets[:smtp_server_address] || "127.0.0.1"
}
+ # Use S3 for storage
+ config.active_storage.service = :local
+
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 210d716078..ed294ecce0 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -87,6 +87,9 @@
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
+ # Use S3 for storage
+ config.active_storage.service = :amazon
+
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
diff --git a/config/environments/staging.rb b/config/environments/staging.rb
index dd4f577d81..d37d310f25 100644
--- a/config/environments/staging.rb
+++ b/config/environments/staging.rb
@@ -78,6 +78,9 @@
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
+ # Use S3 for storage
+ config.active_storage.service = :amazon
+
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 0192b1f899..f88a38b46f 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -38,6 +38,9 @@
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
+ # Testing environment for file uploads
+ config.active_storage.service = :test
+
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
diff --git a/config/locales/en.yml b/config/locales/en.yml
index e146122842..6128d940c6 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -37,6 +37,7 @@ en:
download: Download
existing_account: Already have an account?
get_started: Get Started
+ instant_donation: Instant Donation
invalid_totp: Invalid 6-digit code. Please try again.
log_in: Log In
remove: Remove
@@ -472,6 +473,12 @@ en:
saving: Saving ...
redirecting: Redirecting to Uphold for authorization ...
refreshing: Refreshing balances ...
+ instant_donation_modal:
+ headline: Instant Donation
+ intro: Instant Donation allows your fans to donate BAT at a set amount at the time of browsing your properties through the brand banner. This brand banner comes up when the user clicks the BAT icon next to the URL bar. Customize this brand banner with your own voice and image!
+ skip: Skip for now
+ preview: Preview Banner
+ continue: Continue
uphold_verified:
uphold_error: There was a problem connecting with Uphold. Please try again.
promo:
diff --git a/config/routes.rb b/config/routes.rb
index 48da6c434f..230edfebbf 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -40,7 +40,15 @@
resources :totp_authentications, only: %i(create)
resources :promo_registrations, only: %i(index create)
end
+ resources :site_banners, only: %i(new create), controller: 'publishers/site_banners' do
+ collection do
+ get :fetch
+ post :update_logo
+ post :update_background_image
+ end
+ end
end
+
devise_for :publishers, only: :omniauth_callbacks, controllers: { omniauth_callbacks: "publishers/omniauth_callbacks" }
resources :channels, only: %i(destroy) do
diff --git a/config/secrets.yml b/config/secrets.yml
index c3f9ac7942..a35c8fe6ef 100644
--- a/config/secrets.yml
+++ b/config/secrets.yml
@@ -28,11 +28,14 @@ default: &default
basic_auth_password: <%= ENV["BASIC_AUTH_PASSWORD"] %>
basic_auth_user: <%= ENV["BASIC_AUTH_USER"] %>
fee_rate: <%= ENV["FEE_RATE"] %>
+ brave_rewards_email_whitelist: <%= ENV["BRAVE_REWARDS_EMAIL_WHITELIST"] %>
internal_email: <%= ENV["INTERNAL_EMAIL"] %>
# Piwik analytics endpoint with trailing slash e.g. https://piwik.example.com/
piwik_host: <%= ENV["PIWIK_HOST"] %>
piwik_token: <%= ENV["PIWIK_TOKEN"] %>
proxy_url: <%= ENV["PROXY_URL"] %>
+ # AWS S3 sttings
+ s3_rewards_public_domain: <%= ENV["S3_REWARDS_PUBLIC_DOMAIN"] %>
#SMTP Mailer settings
smtp_server_port: <%= ENV["SENDGRID_SMTP_PORT"] %>
smtp_server_address: <%= ENV["SENDGRID_SMTP_SERVER"] %>
diff --git a/config/storage.yml b/config/storage.yml
new file mode 100644
index 0000000000..7b13b0a8e4
--- /dev/null
+++ b/config/storage.yml
@@ -0,0 +1,14 @@
+amazon:
+ service: S3
+ access_key_id: <%= ENV['S3_REWARDS_ACCESS_KEY_ID'] %>
+ secret_access_key: <%= ENV['S3_REWARDS_SECRET_ACCESS_KEY'] %>
+ region: us-east-2
+ bucket: <%= ENV['S3_REWARDS_BUCKET_NAME'] %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
diff --git a/config/webpack/environment.js b/config/webpack/environment.js
index d16d9af743..e74f57146b 100644
--- a/config/webpack/environment.js
+++ b/config/webpack/environment.js
@@ -1,3 +1,5 @@
const { environment } = require('@rails/webpacker')
+const typescript = require('./loaders/typescript')
+environment.loaders.append('typescript', typescript)
module.exports = environment
diff --git a/config/webpack/loaders/typescript.js b/config/webpack/loaders/typescript.js
new file mode 100644
index 0000000000..9400260e1c
--- /dev/null
+++ b/config/webpack/loaders/typescript.js
@@ -0,0 +1,6 @@
+module.exports = {
+ test: /\.(ts|tsx)?(\.erb)?$/,
+ use: [{
+ loader: 'ts-loader'
+ }]
+}
diff --git a/config/webpacker.yml b/config/webpacker.yml
index f1bf267925..40a83bbefe 100644
--- a/config/webpacker.yml
+++ b/config/webpacker.yml
@@ -19,6 +19,7 @@ default: &default
- .js
- .jsx
- .ts
+ - .tsx
- .vue
- .sass
- .scss
diff --git a/db/migrate/20180810063056_create_site_banner.rb b/db/migrate/20180810063056_create_site_banner.rb
new file mode 100644
index 0000000000..7a89e178d3
--- /dev/null
+++ b/db/migrate/20180810063056_create_site_banner.rb
@@ -0,0 +1,13 @@
+class CreateSiteBanner < ActiveRecord::Migration[5.2]
+ def change
+ create_table :site_banners do |t|
+ t.references :publisher, type: :uuid, index: true, null: false
+ t.text :title, null: false
+ t.text :description, null: false
+ t.integer :donation_amounts, array: true, null: false
+ t.integer :default_donation, null: false
+ t.json :social_links
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20180810070431_create_active_storage_tables.active_storage.rb b/db/migrate/20180810070431_create_active_storage_tables.active_storage.rb
new file mode 100644
index 0000000000..360e0d1b7a
--- /dev/null
+++ b/db/migrate/20180810070431_create_active_storage_tables.active_storage.rb
@@ -0,0 +1,26 @@
+# This migration comes from active_storage (originally 20170806125915)
+class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
+ def change
+ create_table :active_storage_blobs do |t|
+ t.string :key, null: false
+ t.string :filename, null: false
+ t.string :content_type
+ t.text :metadata
+ t.bigint :byte_size, null: false
+ t.string :checksum, null: false
+ t.datetime :created_at, null: false
+
+ t.index [ :key ], unique: true
+ end
+
+ create_table :active_storage_attachments do |t|
+ t.string :name, null: false
+ t.references :record, null: false, polymorphic: true, index: false
+ t.references :blob, null: false
+
+ t.datetime :created_at, null: false
+
+ t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7e4e2443e8..2b408f6f66 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -17,6 +17,27 @@
enable_extension "plpgsql"
enable_extension "uuid-ossp"
+ create_table "active_storage_attachments", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "record_type", null: false
+ t.bigint "record_id", null: false
+ t.bigint "blob_id", null: false
+ t.datetime "created_at", null: false
+ t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
+ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
+ end
+
+ create_table "active_storage_blobs", force: :cascade do |t|
+ t.string "key", null: false
+ t.string "filename", null: false
+ t.string "content_type"
+ t.text "metadata"
+ t.bigint "byte_size", null: false
+ t.string "checksum", null: false
+ t.datetime "created_at", null: false
+ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
+ end
+
create_table "channels", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t|
t.uuid "publisher_id"
t.boolean "created_via_api", default: false, null: false
@@ -262,6 +283,18 @@
t.index ["updated_at"], name: "index_sessions_on_updated_at"
end
+ create_table "site_banners", force: :cascade do |t|
+ t.uuid "publisher_id", null: false
+ t.text "title", null: false
+ t.text "description", null: false
+ t.integer "donation_amounts", null: false, array: true
+ t.integer "default_donation", null: false
+ t.json "social_links"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["publisher_id"], name: "index_site_banners_on_publisher_id"
+ end
+
create_table "site_channel_details", id: :uuid, default: -> { "uuid_generate_v4()" }, force: :cascade do |t|
t.string "brave_publisher_id"
t.string "brave_publisher_id_unnormalized"
diff --git a/package.json b/package.json
index 7c9d9af58c..9c1bc16071 100644
--- a/package.json
+++ b/package.json
@@ -5,9 +5,19 @@
},
"dependencies": {
"@rails/webpacker": "3.5",
+ "@types/react": "^16.4.7",
+ "@types/react-dom": "^16.0.6",
+ "babel-preset-react": "^6.24.1",
+ "brave-ui": "^0.23.0",
"clipboard": "^1.7.1",
"chart.js": "^2.7.2",
- "rails-ujs": "^5.1.4"
+ "prop-types": "^15.6.2",
+ "rails-ujs": "^5.1.4",
+ "react": "^16.4.1",
+ "react-dom": "^16.4.1",
+ "styled-components": "^3.2.6",
+ "ts-loader": "^3.5.0",
+ "typescript": "^2.9.2"
},
"devDependencies": {
"hoek": "^5.0.3",
diff --git a/test/controllers/publishers/site_banners_controller_test.rb b/test/controllers/publishers/site_banners_controller_test.rb
new file mode 100644
index 0000000000..531f268355
--- /dev/null
+++ b/test/controllers/publishers/site_banners_controller_test.rb
@@ -0,0 +1,58 @@
+require "test_helper"
+
+class PublishersControllerTest < ActionDispatch::IntegrationTest
+ include Devise::Test::IntegrationHelpers
+ include PublishersHelper
+
+ test "publisher saves a site banner and the data is consistent" do
+ publisher = publishers(:completed)
+ sign_in publisher
+
+ get home_publishers_path
+ assert_response :success
+
+ post(publisher_site_banners_path(publisher),
+ params: {
+ title: "Hello World",
+ description: "Lorem Ipsum",
+ donation_amounts: "[5, 10, 15]",
+ }
+ )
+
+ assert_response :success
+ publisher.reload
+ assert_equal publisher.site_banner, SiteBanner.last
+ end
+
+ test "publisher cannot upload an excessively large file" do
+ publisher = publishers(:completed)
+ sign_in publisher
+
+ get home_publishers_path
+ assert_response :success
+
+ site_banner = site_banners(:completed)
+
+ fake_data = "A" * Publishers::SiteBannersController::MAX_IMAGE_SIZE
+ post(update_logo_publisher_site_banners_path(publisher.id), params: {image: "data:image/jpeg;base64," + fake_data})
+
+ publisher.reload
+ assert_nil publisher.site_banner.logo.attachment
+ end
+
+ test "publisher can upload a normally sized file" do
+ publisher = publishers(:completed)
+ sign_in publisher
+
+ get home_publishers_path
+ assert_response :success
+
+ site_banner = site_banners(:completed)
+
+ fake_data = "A" * (Publishers::SiteBannersController::MAX_IMAGE_SIZE / 10)
+ post(update_logo_publisher_site_banners_path(publisher.id), params: {image: "data:image/jpg;base64," + fake_data})
+
+ publisher.reload
+ assert_not_nil publisher.site_banner.logo
+ end
+end
diff --git a/test/features/publishers_home_test.rb b/test/features/publishers_home_test.rb
index 1153b621ad..de330bc46c 100644
--- a/test/features/publishers_home_test.rb
+++ b/test/features/publishers_home_test.rb
@@ -204,5 +204,16 @@ class PublishersHomeTest < Capybara::Rails::TestCase
Rails.application.secrets[:api_eyeshade_offline] = prev_api_eyeshade_offline
end
end
+
+ test "only see instant donation button when part of the whitelist" do
+ publisher = publishers(:completed)
+ sign_in publisher
+ visit home_publishers_path
+ refute_content page, "Instant Donation"
+
+ Rails.application.secrets[:brave_rewards_email_whitelist] = publisher.email
+ visit home_publishers_path
+ assert_content page, "Instant Donation"
+ end
end
diff --git a/test/fixtures/site_banners.yml b/test/fixtures/site_banners.yml
new file mode 100644
index 0000000000..fbaa54d25a
--- /dev/null
+++ b/test/fixtures/site_banners.yml
@@ -0,0 +1,14 @@
+default: &default
+ publisher: default
+ title: "Hello World"
+ description: "Lorem Ipsum"
+ donation_amounts: [1, 5, 10]
+ default_donation: 5
+
+completed:
+ publisher: completed
+ title: "Hello World"
+ description: "Lorem Ipsum"
+ donation_amounts: [1, 5, 10]
+ default_donation: 5
+ social_links: {twitter: "BrendanEich"}
diff --git a/test/helpers/image_conversion_helper_test.rb b/test/helpers/image_conversion_helper_test.rb
new file mode 100644
index 0000000000..72ac3a6c72
--- /dev/null
+++ b/test/helpers/image_conversion_helper_test.rb
@@ -0,0 +1,93 @@
+require 'test_helper'
+class ImageConversionHelperTest < ActionView::TestCase
+ test "resizes png to jpg" do
+ source_image_path = "./app/assets/images/bat-logo@3x.png"
+ temp_file_path = resize_to_dimensions_and_convert_to_jpg(
+ source_image_path: source_image_path.to_s,
+ attachment_type: SiteBanner::LOGO,
+ filename: "bat_logo")
+
+ mini_magick_image = MiniMagick::Image.open(temp_file_path)
+ assert_operator SiteBanner::LOGO_DIMENSIONS.first, :>=, mini_magick_image.resolution.first
+ assert_operator SiteBanner::LOGO_DIMENSIONS.second, :>=, mini_magick_image.resolution.second
+ assert "jpg", File.extname(source_image_path)
+ end
+
+ test "resizes a jpg to jpg" do
+ source_image_path = "./app/assets/images/brave-lion@3x.jpg"
+ temp_file_path = resize_to_dimensions_and_convert_to_jpg(
+ source_image_path: source_image_path.to_s,
+ attachment_type: SiteBanner::LOGO,
+ filename: "brave_lion_logo"
+ )
+
+ mini_magick_image = MiniMagick::Image.open(temp_file_path)
+ assert_operator SiteBanner::LOGO_DIMENSIONS.first, :>=, mini_magick_image.resolution.first
+ assert_operator SiteBanner::LOGO_DIMENSIONS.second, :>=, mini_magick_image.resolution.second
+ assert "jpg", File.extname(source_image_path)
+ end
+
+ test "adds padding to a logo for the correct file size" do
+ source_image_path = "./app/assets/images/brave-lion@3x.jpg"
+ mini_magick_image = MiniMagick::Image.open(source_image_path)
+ mini_magick_image.format "jpg"
+ temp_file = Tempfile.new(["brave_lion_copy", ".jpg"])
+
+ mini_magick_image.write(temp_file)
+
+ temp_file_path = resize_to_dimensions_and_convert_to_jpg(
+ source_image_path: temp_file.path,
+ attachment_type: SiteBanner::LOGO,
+ filename: "brave_lion_logo"
+ )
+
+ add_padding_to_image(
+ source_image_path: temp_file_path,
+ attachment_type: SiteBanner::LOGO
+ )
+
+ assert_equal SiteBanner::LOGO_UNIVERSAL_FILE_SIZE, File.open(temp_file_path, 'r').size
+ end
+
+ test "adds padding to a background image for the correct file size" do
+ source_image_path = "./app/assets/images/san_francisco.jpg"
+ mini_magick_image = MiniMagick::Image.open(source_image_path)
+ mini_magick_image.format "jpg"
+ temp_file = Tempfile.new(["san_francisco_copy", ".jpg"])
+
+ mini_magick_image.write(temp_file)
+
+ temp_file_path = resize_to_dimensions_and_convert_to_jpg(
+ source_image_path: temp_file.path,
+ attachment_type: SiteBanner::BACKGROUND,
+ filename: "bat-logo"
+ )
+
+ add_padding_to_image(
+ source_image_path: temp_file_path,
+ attachment_type: SiteBanner::BACKGROUND
+ )
+
+ assert_equal SiteBanner::BACKGROUND_UNIVERSAL_FILE_SIZE, File.open(temp_file_path, 'r').size
+ end
+
+ test "raises exception when attempting to submit a pad an exceptionally large image" do
+ source_image_path = "./app/assets/images/san_francisco.jpg"
+ mini_magick_image = MiniMagick::Image.open(source_image_path)
+ mini_magick_image.format "jpg"
+ temp_file = Tempfile.new(["san_francisco_copy", ".jpg"])
+
+ mini_magick_image.write(temp_file)
+
+ assert_raise ImageConversionHelper::OutsidePaddingRangeError do
+ add_padding_to_image(
+ source_image_path: temp_file.path,
+ attachment_type: SiteBanner::LOGO
+ )
+ end
+ end
+
+ test "generates a consistent filename for an image" do
+ assert_equal "8957c0ef46cdabe73f93ee92b9a43ccb7fa8c9c319212c1f01a67957cab3b6b9", generate_filename(source_image_path: "./app/assets/images/brave-lion@3x.jpg")
+ end
+end
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000000..7425c2b4b1
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "declaration": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "lib": ["es6", "dom"],
+ "module": "es6",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "target": "es5",
+ "jsx": "react"
+ },
+ "exclude": [
+ "**/*.spec.ts",
+ "node_modules",
+ "vendor",
+ "public"
+ ],
+ "compileOnSave": false
+}
diff --git a/yarn.lock b/yarn.lock
index b8298b4f36..9e3b0e2cf1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -33,6 +33,23 @@
webpack "^3.12.0"
webpack-manifest-plugin "^1.3.2"
+"@types/node@*":
+ version "10.5.4"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.4.tgz#6eccc158504357d1da91434d75e86acde94bb10b"
+
+"@types/react-dom@^16.0.6":
+ version "16.0.6"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
+ dependencies:
+ "@types/node" "*"
+ "@types/react" "*"
+
+"@types/react@*", "@types/react@^16.4.7":
+ version "16.4.7"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.7.tgz#f33f6d759a7e1833befa15224d68942d178a5a3f"
+ dependencies:
+ csstype "^2.2.0"
+
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@@ -290,6 +307,10 @@ arraybuffer.slice@~0.0.7:
resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+
asn1.js@^4.0.0:
version "4.9.2"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a"
@@ -503,6 +524,14 @@ babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
+babel-helper-builder-react-jsx@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ esutils "^2.0.2"
+
babel-helper-call-delegate@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
@@ -650,6 +679,14 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0:
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=
+babel-plugin-syntax-flow@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
+
+babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+
babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@@ -878,6 +915,13 @@ babel-plugin-transform-exponentiation-operator@^6.22.0:
babel-plugin-syntax-exponentiation-operator "^6.8.0"
babel-runtime "^6.22.0"
+babel-plugin-transform-flow-strip-types@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
+ dependencies:
+ babel-plugin-syntax-flow "^6.18.0"
+ babel-runtime "^6.22.0"
+
babel-plugin-transform-object-rest-spread@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
@@ -886,6 +930,34 @@ babel-plugin-transform-object-rest-spread@^6.26.0:
babel-plugin-syntax-object-rest-spread "^6.8.0"
babel-runtime "^6.26.0"
+babel-plugin-transform-react-display-name@^6.23.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-self@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-source@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3"
+ dependencies:
+ babel-helper-builder-react-jsx "^6.24.1"
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
babel-plugin-transform-regenerator@^6.22.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
@@ -946,6 +1018,23 @@ babel-preset-env@^1.7.0:
invariant "^2.2.2"
semver "^5.3.0"
+babel-preset-flow@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d"
+ dependencies:
+ babel-plugin-transform-flow-strip-types "^6.22.0"
+
+babel-preset-react@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.3.13"
+ babel-plugin-transform-react-display-name "^6.23.0"
+ babel-plugin-transform-react-jsx "^6.24.1"
+ babel-plugin-transform-react-jsx-self "^6.22.0"
+ babel-plugin-transform-react-jsx-source "^6.22.0"
+ babel-preset-flow "^6.23.0"
+
babel-register@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
@@ -1034,9 +1123,8 @@ base64-arraybuffer@0.1.5:
integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
base64-js@^1.0.2:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
- integrity sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
base64id@1.0.0:
version "1.0.0"
@@ -1226,6 +1314,12 @@ braces@^2.3.0, braces@^2.3.1:
split-string "^3.0.2"
to-regex "^3.0.1"
+brave-ui@^0.23.0:
+ version "0.23.2"
+ resolved "https://registry.yarnpkg.com/brave-ui/-/brave-ui-0.23.2.tgz#a666805f117356abe606dd33ea4e303b99231f93"
+ dependencies:
+ emptykit.css "^1.0.1"
+
brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -1337,6 +1431,13 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
+buffer@^5.0.3:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.0.tgz#53cf98241100099e9eeae20ee6d51d21b16e541e"
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+
buildmail@4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/buildmail/-/buildmail-4.0.1.tgz#877f7738b78729871c9a105e3b837d2be11a7a72"
@@ -1945,6 +2046,10 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+
core-js@^2.2.0:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
@@ -2058,6 +2163,10 @@ css-color-function@~1.3.3:
debug "^3.1.0"
rgb "~0.1.0"
+css-color-keywords@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
+
css-color-names@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
@@ -2092,6 +2201,14 @@ css-selector-tokenizer@^0.7.0:
fastparse "^1.1.1"
regexpu-core "^1.0.0"
+css-to-react-native@^2.0.3:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.2.1.tgz#7f3f4c95de65501b8720c87bf0caf1f39073b88e"
+ dependencies:
+ css-color-keywords "^1.0.0"
+ fbjs "^0.8.5"
+ postcss-value-parser "^3.3.0"
+
css-unit-converter@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
@@ -2148,6 +2265,10 @@ csso@~2.3.1:
clap "^1.0.9"
source-map "^0.5.3"
+csstype@^2.2.0:
+ version "2.5.6"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.6.tgz#2ae1db2319642d8b80a668d2d025c6196071e788"
+
currently-unhandled@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -2465,11 +2586,21 @@ emojis-list@^2.0.0:
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
+emptykit.css@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/emptykit.css/-/emptykit.css-1.0.1.tgz#e7c592c9c4bbe9b358411eb50e5011719a89f512"
+
encodeurl@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
integrity sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=
+encoding@^0.1.11:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+ dependencies:
+ iconv-lite "~0.4.13"
+
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206"
@@ -2519,7 +2650,7 @@ engine.io@~3.1.0:
optionalDependencies:
uws "~9.14.0"
-enhanced-resolve@^3.4.0:
+enhanced-resolve@^3.0.0, enhanced-resolve@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
integrity sha1-BCHjOf1xQZs9oT0Smzl5BAIwR24=
@@ -2970,6 +3101,18 @@ faye-websocket@~0.11.0:
dependencies:
websocket-driver ">=0.5.1"
+fbjs@^0.8.16, fbjs@^0.8.5:
+ version "0.8.17"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.18"
+
file-loader@^1.1.11:
version "1.1.11"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8"
@@ -3639,6 +3782,10 @@ hoek@^5.0.3:
resolved "https://registry.yarnpkg.com/hoek/-/hoek-5.0.3.tgz#b71d40d943d0a95da01956b547f83c4a5b4a34ac"
integrity sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw==
+hoist-non-react-statics@^2.5.0:
+ version "2.5.5"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+
home-or-tmp@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
@@ -3798,7 +3945,7 @@ iconv-lite@0.4.19:
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==
-iconv-lite@0.4.23, iconv-lite@^0.4.4:
+iconv-lite@0.4.23, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
version "0.4.23"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==
@@ -3818,9 +3965,8 @@ icss-utils@^2.1.0:
postcss "^6.0.1"
ieee754@^1.1.4:
- version "1.1.8"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
- integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
iferr@^0.1.5:
version "0.1.5"
@@ -4213,7 +4359,7 @@ is-regex@^1.0.4:
dependencies:
has "^1.0.1"
-is-stream@^1.1.0:
+is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
@@ -4292,6 +4438,13 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -4307,7 +4460,11 @@ js-reporters@1.2.1:
resolved "https://registry.yarnpkg.com/js-reporters/-/js-reporters-1.2.1.tgz#f88c608e324a3373a95bcc45ad305e5c979c459b"
integrity sha1-+IxgjjJKM3OpW8xFrTBeXJecRZs=
-js-tokens@^3.0.0, js-tokens@^3.0.2:
+"js-tokens@^3.0.0 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+
+js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
@@ -4725,12 +4882,11 @@ longest@^1.0.1:
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
-loose-envify@^1.0.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
- integrity sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
dependencies:
- js-tokens "^3.0.0"
+ js-tokens "^3.0.0 || ^4.0.0"
loud-rejection@^1.0.0, loud-rejection@^1.6.0:
version "1.6.0"
@@ -5163,6 +5319,13 @@ netmask@^1.0.6:
resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
node-forge@0.6.33:
version "0.6.33"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc"
@@ -6491,6 +6654,12 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ dependencies:
+ asap "~2.0.3"
+
promisify-call@^2.0.2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/promisify-call/-/promisify-call-2.0.4.tgz#d48c2d45652ccccd52801ddecbd533a6d4bd5fba"
@@ -6498,6 +6667,13 @@ promisify-call@^2.0.2:
dependencies:
with-callback "^1.0.2"
+prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
+ dependencies:
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
proxy-addr@~2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
@@ -6730,6 +6906,28 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
+react-dom@^16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
+react-is@^16.3.1:
+ version "16.4.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88"
+
+react@^16.4.1:
+ version "16.4.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.1"
+ prop-types "^15.6.0"
+
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
@@ -7284,7 +7482,7 @@ selfsigned@^1.9.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
integrity sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==
-semver@^5.5.0:
+semver@^5.0.1, semver@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
@@ -7371,7 +7569,7 @@ set-value@^2.0.0:
is-plain-object "^2.0.3"
split-string "^3.0.1"
-setimmediate@^1.0.4:
+setimmediate@^1.0.4, setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -7889,6 +8087,28 @@ style-loader@^0.21.0:
loader-utils "^1.1.0"
schema-utils "^0.4.5"
+styled-components@^3.2.6:
+ version "3.4.2"
+ resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.4.2.tgz#8f518419932327e47fe9144824e3184b3e2da95d"
+ dependencies:
+ buffer "^5.0.3"
+ css-to-react-native "^2.0.3"
+ fbjs "^0.8.16"
+ hoist-non-react-statics "^2.5.0"
+ prop-types "^15.5.4"
+ react-is "^16.3.1"
+ stylis "^3.5.0"
+ stylis-rule-sheet "^0.0.10"
+ supports-color "^3.2.3"
+
+stylis-rule-sheet@^0.0.10:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430"
+
+stylis@^3.5.0:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.3.tgz#99fdc46afba6af4deff570825994181a5e6ce546"
+
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -8087,6 +8307,16 @@ trim-right@^1.0.1:
dependencies:
glob "^6.0.4"
+ts-loader@^3.5.0:
+ version "3.5.0"
+ resolved "http://registry.npmjs.org/ts-loader/-/ts-loader-3.5.0.tgz#151d004dcddb4cf8e381a3bf9d6b74c2d957a9c0"
+ dependencies:
+ chalk "^2.3.0"
+ enhanced-resolve "^3.0.0"
+ loader-utils "^1.0.2"
+ micromatch "^3.1.4"
+ semver "^5.0.1"
+
tsscmp@~1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97"
@@ -8142,6 +8372,14 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+typescript@^2.9.2:
+ version "2.9.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
+
+ua-parser-js@^0.7.18:
+ version "0.7.18"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.18.tgz#a7bfd92f56edfb117083b69e31d2aa8882d4b1ed"
+
uglify-es@^3.3.4:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
@@ -8550,6 +8788,10 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+whatwg-fetch@>=0.10.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
+
when@^3.7.7:
version "3.7.8"
resolved "https://registry.yarnpkg.com/when/-/when-3.7.8.tgz#c7130b6a7ea04693e842cdc9e7a1f2aa39a39f82"