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 c806f170dc..4caa477515 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,9 @@ gem "api-pagination" # Encrypt DB data at rest gem "attr_encrypted", "~> 3.1.0" +# Use AWS gem for s3 uploads +gem 'aws-sdk-s3', require: false + gem "bootstrap", "~> 4.1.1" # browser details diff --git a/Gemfile.lock b/Gemfile.lock index ff2f8c0179..76799cc314 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,21 @@ GEM encryptor (~> 3.0.0) autoprefixer-rails (9.0.0) execjs + aws-eventstream (1.0.1) + aws-partitions (1.98.0) + aws-sdk-core (3.24.1) + aws-eventstream (~> 1.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-kms (1.7.0) + aws-sdk-core (~> 3) + aws-sigv4 (~> 1.0) + aws-sdk-s3 (1.17.0) + aws-sdk-core (~> 3, >= 3.21.2) + 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) @@ -158,6 +173,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) @@ -435,6 +451,7 @@ DEPENDENCIES activerecord-session_store api-pagination attr_encrypted (~> 3.1.0) + aws-sdk-s3 bootstrap (~> 4.1.1) brakeman browser 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/stylesheets/pages/home.scss b/app/assets/stylesheets/pages/home.scss index 7594ed853b..edc0ad5fa3 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..55db323522 --- /dev/null +++ b/app/controllers/publishers/site_banners_controller.rb @@ -0,0 +1,56 @@ +class Publishers::SiteBannersController < ApplicationController + 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[1], + social_links: params[:social_links], + description: params[:description] + ) + head :ok + end + + def update_logo + site_banner = current_publisher.site_banner + update_image(site_banner.logo) + head :ok + end + + def update_background_image + site_banner = current_publisher.site_banner + update_image(site_banner.background_image) + head :ok + end + + private + + def update_image(attachment) + 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" + else + # TODO: Throw an exception here + end + filename = Time.now.to_s.gsub!(" ", "_").gsub!(":", "_") + current_publisher.id + "_logo" + + file = Tempfile.new([filename, extension]) + File.open(file.path, 'wb') do |f| + f.write(Base64.decode64(params[:image].split(',')[1])) + end + attachment.attach(io: open(file.path), + filename: filename, + content_type: content_type + ) + end +end diff --git a/app/javascript/locale/en.js b/app/javascript/locale/en.js new file mode 100644 index 0000000000..4f5e6d77cc --- /dev/null +++ b/app/javascript/locale/en.js @@ -0,0 +1,86 @@ +const locale = { + about: 'about', + addFunds: 'add funds', + allowTip: 'Allow tips on', + braveRewards: 'Brave Rewards', + cancel: 'Cancel', + claim: 'Claim', + copy: 'Copy', + currentDonation: 'You’re currently donating {{currentDonation}} BAT to this site every month.', + detail: 'Detail', + donationAmount: 'Donation amount', + done: 'Done', + earningsAds: 'Earnings from Brave Ads', + expiresOn: 'expires on', + import: 'import', + makeMonthly: 'Make this monthly', + monthApr: 'Apr', + monthAug: 'August', + monthDec: 'December', + monthFeb: 'February', + monthJan: 'January', + monthJul: 'July', + monthJun: 'June', + monthMar: 'March', + monthMay: 'May', + monthNov: 'November', + monthOct: 'October', + monthSep: 'September', + noGrants: 'Currently no token grant is available.', + notEnoughTokens: 'Not enough tokens. Please', + on: 'on', + oneTime: 'One time', + oneTimeDonation: 'One-time Donations/Tips', + print: 'Print', + recoveryKeys: 'Recovery Key', + recurring: 'Recurring', + recurringDonations: 'Recurring Donations', + remove: 'remove', + restore: 'Restore', + rewardsBackupText1: 'Backup your Wallet', + rewardsBackupText2: 'Keep this anonymized recovery key for your Brave wallet in the safe place in case you lose access to this browser. Your funds are safe as long as you keep this recovery key either on a paper or in a device with preferably no internet connection.', + rewardsBannerText1: 'Thanks for stopping by. We joined Brave’s vision of protecting your privacy because we believe that fans like you would support us in our effort to keep the web a clean and safe place to be.', + rewardsBannerText2: 'Your donation is much appreciated and it encourages us to continue to improve our content.', + rewardsContribute: 'Brave Contribute', + rewardsContributeAttention: 'Your attention metric', + rewardsContributeText1: 'You’re currently supporting', + rewardsContributeVisited: 'Site visited', + rewardsOffText1: 'Do you know that you’ve been paying for the web content with your data for the digital ads? You didn’t have a voice in it and worse, you’re exposed to privacy and security risks.', + rewardsOffText2: 'Brave Rewards allows you to take control back.', + rewardsOffText3: 'How does it work?', + rewardsOffText4: 'Your attention is valuable. Get paid for the ads. And pay directly the favorite content creators at your will. That way, they can grow and continue delivering the content that delights you.', + rewardsPanelEmptyText1: 'Sadly, no tokens yet.', + rewardsPanelEmptyText2: '3 ways to fill your wallet:', + rewardsPanelEmptyText3: 'You can add funds.', + rewardsPanelEmptyText4: 'You can earn tokens from Brave Ads.', + rewardsPanelEmptyText5: 'Occasionally, you will also received token grants from Brave. So keep an eye out for the alert!', + rewardsPanelOffText1: 'Get Rewarded for Browsing!', + rewardsPanelOffText2: 'Earn tokens for your attention to ads and pay it forward to support content creators you value!', + rewardsPanelText1: 'Add, withdraw and manage funds at', + rewardsPanelText2: 'Brave wallet is managed by', + rewardsRestoreText1: 'Restore your Wallet', + rewardsRestoreText2: 'Enter the recovery key to restore your Brave wallet. Make sure that the current wallet doesn’t have any balance or is backed up safely. Restoring a wallet replaces the current wallet and you will loose any balance if you don’t back up its recovery key.', + rewardsRestoreText3: 'Enter your recovery key or', + rewardsSummary: 'Rewards Summary', + rewardsWhy: 'Why Brave Rewards…', + saveAsFile: 'Save as File', + seeAllItems: 'See all {{numItems}} items', + seeAllSites: 'See all {{numSites}} sites', + sendDonation: 'Send my donation', + sendTip: 'Send my Tip', + settings: 'Settings', + siteVisited: 'Site visited', + sites: 'sites', + tipOnLike: 'Tip on like', + tokenBalance: 'Token balance', + tokenGrant: 'Token Grant', + tokens: 'tokens', + type: 'Type', + verifiedPublisher: 'Brave Verified Publisher', + walletActivity: 'Wallet Activity/ Monthly Statement', + walletBalance: 'wallet balance', + welcome: 'Welcome!', + yourWallet: 'Your wallet' +}; + +export default locale; 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..4a096af3d3 --- /dev/null +++ b/app/javascript/packs/brave_rewards_banner_display.jsx @@ -0,0 +1,480 @@ +// 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.updateDescription = this.updateDescription.bind(this); + this.updateTwitch = this.updateTwitch.bind(this); + this.updateYoutube = this.updateYoutube.bind(this); + this.updateTwitter = this.updateTwitter.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 + }) + ); + } + + 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(' + bgUrl + ')'; + + //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(); + } + + 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(); + + if (this.props.editMode) { + this.setupBackgroundLabel(); + this.setupLogoLabel(); + + // Set h3 editable + document.getElementsByClassName("sc-gZMcBi")[0].setAttribute("contenteditable", true) + + // 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}) + } + + 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}); + } + + /* + setTextsFromDiv() { + this.setState({ + title: document.getElementsByClassName("sc-gZMcBi")[0].innerText, + description: document.getElementsByClassName("sc-gqjmRU")[0].innerText + }); + } + */ + + handleSubmit(event) { + const url = '/publishers/' + this.props.publisher_id + "/site_banners"; + var request = new XMLHttpRequest(); + const body = new FormData(); + + body.append('title', document.getElementsByClassName("sc-gZMcBi")[0].innerText); + 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 ( +
+
+
+
+
+

All Channels

+
+
+ {topController} +
+
+
+
+ + + +
+ { + // this.state.socialLinks.twitter !== undefined && +
+ + +
+ } + { + // this.state.socialLinks.youtube !== undefined && +
+ + +
+ } + { + // this.state.socialLinks.twitch !== undefined && +
+ + +
+ } +
+
+
+ + +
+
+ ); + } +} + +/* +
+
+

PREVIEW

+
+ +
+ Description: +