From cdd7b51e3cb33cad04ca48171fab6a12615d36eb Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Tue, 7 Sep 2021 21:59:53 +0530 Subject: [PATCH] feat: GR11 (#9421) * Clean up verbiage (#9309) Clean up verbiage on footer * Bump moment from 2.17.1 to 2.19.3 (#9307) Bumps [moment](https://github.com/moment/moment) from 2.17.1 to 2.19.3. - [Release notes](https://github.com/moment/moment/releases) - [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md) - [Commits](https://github.com/moment/moment/compare/2.17.1...2.19.3) --- updated-dependencies: - dependency-name: moment dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * GITC-223: ignore squelched profiles in clr (#9312) * chore: improve grants search using vector_column * add custom migration * tags/1: add new model + port data * tags/2: remove GrantCategory * fix alert css * add /v1/api/tags endpoint * feat: tweak get_grants to support chain and tagv * re-introduce removed fields * ensure filters work as expected * chore : update old API * add grants layout structure * add clr router * chore: wire in grant tags in create + edit * introduce old filter * backend query applied and basic query * fix tenant query * add filters and parameters logic changes * sort * add grant_region filter * fix broken sort * filters ux * tags router search and filters * collections backend and cache * tab system and collections * chore: move me=true * filter by collection * collections paginatination * remove extra request * update styles * GITC-239: Introduces global nav-cart to replace grants side-cart (#9418) * fix get_hackathons4 * GITC-321: Sets featured grant to 'grant_id = 1' (#9367) * feat: introduce steward endpoint (#9377) * GITC-276 purge: gitcoin faucet (#9333) * fixes bounty fulfillment data (#9368) * GITC-245: Implements the CLR calc in SQL (#9348) * GITC-245: Implements the CLR calc in SQL * GITC-245: Adds trust_bonus to profiles as_dict to optimise clr query * GITC-245: Inner joins * GITC-245: Fixes update_trust_bonus * GITC-245: Adds prints to clr to enable comparisons * GITC-245: Refactors --call-now version of update to happen inline * GITC-245: Refactors clr2 to avoid checking grants with no contribs and to account for grant.defer_clr_to * GITC-245: Adds a hybrid sql/python version of the calc (need to check both in production setting) * GITC-245: Adds 'defer_clr_to' to grants admin page * GITC-245: Adds to print statement to maintain consistency * Moves the id clause outside of the right join * feat: trust-bonus api (#9349) * chore: made defer_clr_to readonly * stale nav (#9382) * GITC-294: show clear cart on loading screen (#9369) * DoingGud interview * Update API.md * GITC-315: Fixes sms verify (#9390) * GITC-368: Adds skip_cleanup to retain build artefacts (#9409) * Fix small bug when click to slack link (#9416) * Updates db.py to always read from replicas (#9403) * GITC-239: Introduces gc-cart-content component Co-authored-by: octavioamu Co-authored-by: Aditya Anand M C Co-authored-by: Kevin Owocki Co-authored-by: Kyle Weiss Co-authored-by: Thien Tran * feat: polygon l2 checkout (#9372) * fix get_hackathons4 * GITC-321: Sets featured grant to 'grant_id = 1' (#9367) * rough commit * setup polygon: connect/switch/add chain * feat: introduce steward endpoint (#9377) * GITC-276 purge: gitcoin faucet (#9333) * fixes bounty fulfillment data (#9368) * GITC-245: Implements the CLR calc in SQL (#9348) * GITC-245: Implements the CLR calc in SQL * GITC-245: Adds trust_bonus to profiles as_dict to optimise clr query * GITC-245: Inner joins * GITC-245: Fixes update_trust_bonus * GITC-245: Adds prints to clr to enable comparisons * GITC-245: Refactors --call-now version of update to happen inline * GITC-245: Refactors clr2 to avoid checking grants with no contribs and to account for grant.defer_clr_to * GITC-245: Adds a hybrid sql/python version of the calc (need to check both in production setting) * GITC-245: Adds 'defer_clr_to' to grants admin page * GITC-245: Adds to print statement to maintain consistency * Moves the id clause outside of the right join * feat: trust-bonus api (#9349) * chore: made defer_clr_to readonly * more utils * upload svg * stale nav (#9382) * GITC-294: show clear cart on loading screen (#9369) * DoingGud interview * Update API.md * implement modal design * checkout recommendation, estimate gas, rootToChildToken * finish checkout + handle unsupported tokens etc. * handle balance check + amount to deposit * GITC-315: Fixes sms verify (#9390) * GITC-368: Adds skip_cleanup to retain build artefacts (#9409) * gas cost estimation * bug fix checkout recommendation * some fixes * Fix small bug when click to slack link (#9416) * Updates db.py to always read from replicas (#9403) * address review comments @thelostone-mc * Add test env to bundle (#9374) * Allow test environment to upload assets to hosted space * Updating pip requirements to deconflict * Fixing issues from PR comments * Fixing issues from PR comments * Changing environment for travis testing to 'travis' * Updating the variable references to be easier to understand. * more review fixes @PixelantDesign * review fix @willsputra * indicate metamask popup * no metamask popup with insufficient balance Co-authored-by: octavioamu Co-authored-by: Graham Dixon Co-authored-by: Graham Dixon Co-authored-by: Aditya Anand M C Co-authored-by: Kevin Owocki Co-authored-by: Kyle Weiss Co-authored-by: Thien Tran Co-authored-by: Zack Schiller <81022539+zacheryschiller@users.noreply.github.com> * GITC-131: Adds Discover Grants section to grants landing (#9386) * GITC-131: Adds Discover Grants section to grants landing * GITC-131: Replaces svgs * lint fixes + URL * fix default value * Fixes border mixin * Corrects discover section query strings * Adds count to checkout button * reset page when filtering * fix following not removing filter * reset pagination on clr filter * filters dropdown styles * me filter * tweak more filter font-size * disable auto-flip based on viewport * chore: reset page on changing filter + fix pre-filling vue store from URL * show idle grants in collections * merge conflict * change clr banner to black * Purges sidecart and ensures interactions with nav-cart are reflected everywhere * introduce my_collections=true * Switches categories for tags on grant details page * update text color to blak * Adds validation for provided eth address on create grant form * fix filters and load tab by url * Adds nav-cart to sticky filter bar, fixes some filtering behaviour and links/layout * Wrap intersection observer to ensure the ref is present * Ensures static url appending is clean * changing sort change page to 1 * remove add all to cart * fix navbar css * Center align the number of grants * move filters to top * add label to tenant * fix filter styles * hide cats if not options * admin bar to top * fix keyboard navigation filter * fix clr links * fix collections links * collections on landingpage * fix placeholder urls * clean dead code * reset page when switching from collections * add link polygon support * GITC-370: polygon gas cost estimation (#9430) * improve gas cost estimation + add mainnet address * check use has sufficient matic for gas * bug fix * remove log * show neglible currency values * fix align on list view * GITC-358: Reintroduces sort methods * Enables history navigation on filter change * Removes random_shuffle from the available sorts * Ensures params are cleared when filter data is empty * Fixes back button/text on grant details * add reverse pagination * remove blank for grants details * Fixes back button for after user follows an external link to a grants details page * fix zksync errors * polygon behind staff flag * GITC-247: Fixes the stats tab for team_members * Fixes button alignment Co-authored-by: Kyle Weiss Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: octavioamu Co-authored-by: Graham Dixon Co-authored-by: Kevin Owocki Co-authored-by: Thien Tran Co-authored-by: Chibuotu Amadi Co-authored-by: Graham Dixon Co-authored-by: Zack Schiller <81022539+zacheryschiller@users.noreply.github.com> --- app/assets/v2/images/grants/polygon.svg | 9 + app/assets/v2/js/base.js | 19 - app/assets/v2/js/cart-data.js | 37 +- app/assets/v2/js/cart-ethereum-polygon.js | 424 +++++++++ app/assets/v2/js/cart-ethereum-zksync.js | 9 +- app/assets/v2/js/cart-nav.js | 50 + app/assets/v2/js/cart.js | 206 +++-- app/assets/v2/js/data-chains.js | 40 + app/assets/v2/js/grants/_detail-component.js | 51 +- app/assets/v2/js/grants/_detail.js | 8 +- app/assets/v2/js/grants/_new.js | 20 +- app/assets/v2/js/grants/components.js | 6 +- .../v2/js/grants/create-collection-modal.js | 2 +- app/assets/v2/js/grants/funding.js | 259 ------ app/assets/v2/js/grants/index.js | 859 +++++++++--------- app/assets/v2/js/grants/landingpage.js | 478 +--------- app/assets/v2/js/grants/new_match.js | 12 +- app/assets/v2/js/shared.js | 47 +- app/assets/v2/js/vue-filters.js | 9 + app/assets/v2/scss/buttons.scss | 31 +- app/assets/v2/scss/gc-mixins.scss | 9 +- app/assets/v2/scss/gc-utilities.scss | 15 + app/assets/v2/scss/grants/collection.scss | 3 +- app/assets/v2/scss/grants/side-cart.scss | 70 -- .../lib/vue-select/global/_variables.scss | 4 +- .../lib/vue-select/modules/_selected.scss | 7 +- .../templates/dashboard/index-vue.html | 2 +- app/dashboard/templates/shared/cart_nav.html | 54 +- app/dashboard/templates/shared/nav_auth.html | 4 +- app/grants/admin.py | 17 +- .../migrations/0123_auto_20210726_0703.py | 61 ++ app/grants/models.py | 36 +- app/grants/router.py | 63 +- app/grants/serializers.py | 55 +- app/grants/templates/grants/_new.html | 13 +- app/grants/templates/grants/cart-vue.html | 3 +- app/grants/templates/grants/cart/eth.html | 142 ++- .../templates/grants/components/card.html | 29 +- .../grants/components/collection.html | 24 +- .../templates/grants/detail/_index.html | 38 +- .../templates/grants/detail/side-cart.html | 44 - .../grants/detail/template-grant-details.html | 36 +- app/grants/templates/grants/explorer.html | 429 +++++++++ app/grants/templates/grants/index.html | 213 ----- app/grants/templates/grants/landingpage.html | 61 +- app/grants/templates/grants/new_match.html | 10 +- .../grants/shared/active_clr_round.html | 6 +- .../grants/shared/landing_grants.html | 29 +- .../grants/shared/landing_navbar.html | 23 - .../grants/shared/landing_qf_active.html | 2 +- .../grants/shared/sidebar_search.html | 202 ---- .../templates/grants/shared/top-filters.html | 93 +- app/grants/tests/test_views.py | 40 - app/grants/urls.py | 13 +- app/grants/views.py | 586 ++++++------ .../management/commands/create_page_cache.py | 14 +- .../templates/shared/footer_scripts.html | 1 + requirements/base.txt | 2 +- 58 files changed, 2655 insertions(+), 2374 deletions(-) create mode 100644 app/assets/v2/images/grants/polygon.svg create mode 100644 app/assets/v2/js/cart-ethereum-polygon.js create mode 100644 app/assets/v2/js/cart-nav.js delete mode 100644 app/assets/v2/scss/grants/side-cart.scss create mode 100644 app/grants/migrations/0123_auto_20210726_0703.py delete mode 100644 app/grants/templates/grants/detail/side-cart.html create mode 100644 app/grants/templates/grants/explorer.html delete mode 100644 app/grants/templates/grants/index.html delete mode 100644 app/grants/templates/grants/shared/landing_navbar.html delete mode 100644 app/grants/templates/grants/shared/sidebar_search.html delete mode 100644 app/grants/tests/test_views.py diff --git a/app/assets/v2/images/grants/polygon.svg b/app/assets/v2/images/grants/polygon.svg new file mode 100644 index 00000000000..57a330be040 --- /dev/null +++ b/app/assets/v2/images/grants/polygon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/assets/v2/js/base.js b/app/assets/v2/js/base.js index 434fc6898cf..b7d856577e8 100644 --- a/app/assets/v2/js/base.js +++ b/app/assets/v2/js/base.js @@ -18,11 +18,6 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // TODO: MOVE TO GRANTS shared - if (typeof CartData != 'undefined') { - applyCartMenuStyles(); - } - $('body').on('click', '.copy_me', function() { $(this).focus(); $(this).select(); @@ -425,20 +420,6 @@ this.gitcoinUpdates = () => { }; -this.applyCartMenuStyles = function() { - let dot = $('#cart-notification-dot'); - - if (CartData.hasItems()) { - dot.addClass('notification__dot_active'); - dot.text(CartData.length()); - } else { - dot.removeClass('notification__dot_active'); - if (document.location.href.indexOf('/grants') == -1) { - $('#cart-nav').addClass('hidden'); - } - } -}; - // Turn form data pulled form page into a JS object this.objectifySerialized = function(data) { let objectData = {}; diff --git a/app/assets/v2/js/cart-data.js b/app/assets/v2/js/cart-data.js index f171e15351f..11459a363a8 100644 --- a/app/assets/v2/js/cart-data.js +++ b/app/assets/v2/js/cart-data.js @@ -52,7 +52,7 @@ this.CartData = class CartData { return bulk_add_cart; } - static addToCart(grantData, no_report) { + static addToCart(grantData, no_report, skipEmit) { if (this.cartContainsGrantWithId(grantData.grant_id)) { return; } @@ -164,7 +164,8 @@ this.CartData = class CartData { let cartList = this.loadCart(); cartList.push(grantData); - this.setCart(cartList); + + this.setCart(cartList, skipEmit); if (!no_report) { fetchData(`/grants/${grantData.grant_id}/activity`, 'POST', { @@ -174,7 +175,7 @@ this.CartData = class CartData { } } - static removeIdFromCart(grantId) { + static removeIdFromCart(grantId, skipEmit) { grantId = String(grantId); let cartList = this.loadCart(); @@ -186,10 +187,10 @@ this.CartData = class CartData { metadata: JSON.stringify(newList) }, { 'X-CSRFToken': $("input[name='csrfmiddlewaretoken']").val() }); - this.setCart(newList); + this.setCart(newList, skipEmit); } - static updateCartItem(grantId, field, value) { + static updateCartItem(grantId, field, value, skipEmit) { let cartList = this.loadCart(); let grant = null; @@ -209,12 +210,16 @@ this.CartData = class CartData { grant[field] = value; - this.setCart(cartList); + this.setCart(cartList, skipEmit); } - - static clearCart() { + + static clearCart(skipEmit) { let cartList = this.loadCart(); + if (!skipEmit) { + this.emitDataUpdate([]); + } + fetchData('/grants/0/activity', 'POST', { action: 'CLEAR_CART', metadata: JSON.stringify(cartList), @@ -222,7 +227,7 @@ this.CartData = class CartData { }, { 'X-CSRFToken': $("input[name='csrfmiddlewaretoken']").val() }); localStorage.setItem('grants_cart', JSON.stringify([])); - applyCartMenuStyles(); + } static loadCart() { @@ -241,9 +246,19 @@ this.CartData = class CartData { return parsedCart; } - static setCart(list) { + static setCart(list, skipEmit) { + if (!skipEmit) { + this.emitDataUpdate(list); + } localStorage.setItem('grants_cart', JSON.stringify(list)); - applyCartMenuStyles(); + } + + static emitDataUpdate(list) { + window.dispatchEvent(new CustomEvent('cartDataUpdated', { + detail: { + list: list + } + })); } static loadCheckedOut() { diff --git a/app/assets/v2/js/cart-ethereum-polygon.js b/app/assets/v2/js/cart-ethereum-polygon.js new file mode 100644 index 00000000000..822f968cfe5 --- /dev/null +++ b/app/assets/v2/js/cart-ethereum-polygon.js @@ -0,0 +1,424 @@ +const bulkCheckoutAddressPolygon = appCart.$refs.cart.network === 'mainnet' + ? '0xb99080b9407436eBb2b8Fe56D45fFA47E9bb8877' + : '0x3E2849E2A489C8fE47F52847c42aF2E8A82B9973'; + +function objectMap(object, mapFn) { + return Object.keys(object).reduce(function(result, key) { + result[key] = mapFn(object[key]); + return result; + }, {}); +} + +Vue.component('grantsCartEthereumPolygon', { + props: { + currentTokens: { type: Array, required: true }, // Array of available tokens for the selected web3 network + donationInputs: { type: Array, required: true }, // donationInputs computed property from cart.js + grantsByTenant: { type: Array, required: true }, // Array of grants in cart + maxCartItems: { type: Number, required: true }, // max number of items in cart + grantsUnderMinimalContribution: { type: Array, required: true } // Array of grants under min contribution + }, + + data: function() { + return { + polygon: { + showModal: false, // true to show modal to user, false to hide + checkoutStatus: 'not-started', // options are 'not-started', 'pending', and 'complete' + estimatedGasCost: 65000 + }, + + cart: { + tokenList: [], // array of tokens in the cart + unsupportedTokens: [] // tokens in cart which are not supported by Polygon + }, + + user: { + requiredAmounts: null + } + }; + }, + + mounted() { + window.addEventListener('beforeunload', (e) => { + if (this.polygon.checkoutStatus === 'pending') { + e.returnValue = 'Polygon checkout in progress. Are you sure you want to leave?'; + } + }); + + // Update Polygon checkout connection, state, and data frontend needs when wallet connection changes + window.addEventListener('dataWalletReady', async(e) => { + await this.onChangeHandler(this.donationInputs); + }); + }, + + computed: { + /** + * @dev List of tokens supported by Polygon + Gitcoin. To add a token to this list: + * 1. Make sure the token is top 10 used tokens based on Gitcoin's historical data + * 2. Confirm the token exists on Polygon's list of supported tokens: https://mapper.matic.today/ + * 2. Add the token symbol to the appropriate list below + * @dev We hardcode the list from Gitcoin's historical data based on the top ten tokens + * on ethereum chain and also Polygon network used by users to checkout + */ + supportedTokens() { + const mainnetTokens = [ 'DAI', 'ETH', 'USDT', 'USDC', 'PAN', 'BNB', 'UNI', 'CELO', 'MASK', 'MATIC' ]; + const testnetTokens = [ 'DAI', 'ETH', 'USDT', 'USDC', 'UNI', 'MATIC' ]; + + return appCart.$refs.cart.network === 'mainnet' ? mainnetTokens : testnetTokens; + }, + + donationInputsNativeAmount() { + return appCart.$refs.cart.donationInputsNativeAmount; + }, + + requiredAmountsString() { + let string = ''; + + requiredAmounts = this.user.requiredAmounts; + Object.keys(requiredAmounts).forEach(key => { + // Round to 2 digits + if (requiredAmounts[key]) { + const amount = requiredAmounts[key]; + const formattedAmount = amount.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + + if (string === '') { + string += `${formattedAmount} ${key}`; + } else { + string += ` + ${formattedAmount} ${key}`; + } + } + }); + return string; + } + }, + + watch: { + // Watch donationInputs prop so we can update cart.js as needed on changes + donationInputs: { + immediate: true, + async handler(donations) { + // Update state and data that frontend needs + await this.onChangeHandler(donations); + } + }, + + // When network changes we need to update Polygon config, fetch new balances, etc. + network: { + immediate: true, + async handler() { + await this.onChangeHandler(this.donationInputs); + } + } + }, + + methods: { + initWeb3() { + let url; + + if (appCart.$refs.cart.network === 'mainnet') { + appCart.$refs.cart.networkId = '137'; + url = 'https://rpc-mainnet.maticvigil.com'; + } else { + appCart.$refs.cart.networkId = '80001'; + appCart.$refs.cart.network = 'testnet'; + url = 'https://rpc-mumbai.maticvigil.com'; + } + + return new Web3(url); + }, + + openBridgeUrl() { + let url = appCart.$refs.cart.network == 'mainnet' + ? 'https://wallet.matic.network/bridge' + : 'https://wallet.matic.today/bridge'; + + window.open(url, '_blank'); + this.polygon.checkoutStatus = 'depositing'; + }, + + handleError(e) { + appCart.$refs.cart.handleError(e); + }, + + getDonationInputs() { + return appCart.$refs.cart.getDonationInputs(); + }, + + getTokenByName(name) { + return appCart.$refs.cart.getTokenByName(name, true); + }, + + async postToDatabase(txHash, contractAddress, userAddress) { + await appCart.$refs.cart.postToDatabase(txHash, contractAddress, userAddress, 'eth_polygon'); + }, + + async finalizeCheckout() { + await appCart.$refs.cart.finalizeCheckout(); + }, + + async getAllowanceData(userAddress, targetContract) { + return await appCart.$refs.cart.getAllowanceData(userAddress, targetContract, true); + }, + + async requestAllowanceApprovalsThenExecuteCallback( + allowanceData, userAddress, targetContract, callback, callbackParams + ) { + return await appCart.$refs.cart.requestAllowanceApprovalsThenExecuteCallback( + allowanceData, userAddress, targetContract, callback, callbackParams + ); + }, + + // We want to run this whenever wallet or cart content changes + async onChangeHandler(donations) { + // Get array of token symbols based on cart data. For example, if the user has two + // DAI grants and one ETH grant in their cart, this returns `[ 'DAI', 'ETH' ]` + this.cart.tokenList = [...new Set(donations.map((donation) => donation.name))]; + + // Get list of tokens in cart not supported by Polygon + this.cart.unsupportedTokens = this.cart.tokenList.filter( + (token) => !this.supportedTokens.includes(token) + ); + + // Update the fee estimate and gas cost based on changes + this.polygon.estimatedGasCost = await this.estimateGasCost(); + + // Emit event so cart.js can update state accordingly to display info to user + this.$emit('polygon-data-updated', { + polygonUnsupportedTokens: this.cart.unsupportedTokens, + polygonEstimatedGasCost: this.polygon.estimatedGasCost + }); + }, + + // Reset Polygon modal status after a checkout failure + resetPolygonModal() { + this.polygon.checkoutStatus = 'not-started'; + }, + + closePolygonModal() { + this.polygon.showModal = false; + }, + + async setupPolygon() { + indicateMetamaskPopup(); + // Connect to Polygon network with MetaMask + const network = appCart.$refs.cart.network; + let chainId = network === 'mainnet' ? '0x89' : '0x13881'; + let rpcUrl = network === 'mainnet' ? 'https://rpc-mainnet.maticvigil.com' + : 'https://rpc-mumbai.maticvigil.com'; + + try { + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId }] + }); + } catch (switchError) { + // This error code indicates that the chain has not been added to MetaMask + if (switchError.code === 4902) { + let networkText = network === 'rinkeby' || network === 'goerli' || + network === 'ropsten' || network === 'kovan' ? 'testnet' : network; + + try { + await ethereum.request({ + method: 'wallet_addEthereumChain', + params: [{ + chainId, + rpcUrls: [rpcUrl], + chainName: `Polygon ${networkText.replace(/\b[a-z]/g, (x) => x.toUpperCase())}`, + nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 } + }] + }); + } catch (addError) { + if (addError.code === 4001) { + throw new Error('Please connect MetaMask to Polygon network.'); + } else { + console.error(addError); + } + } + } else if (switchError.code === 4001) { + throw new Error('Please connect MetaMask to Polygon network.'); + } else { + console.error(switchError); + } + } + }, + + // Send a batch transfer based on donation inputs + async checkoutWithPolygon() { + try { + + if (typeof ga !== 'undefined') { + ga('send', 'event', 'Grant Checkout', 'click', 'Person'); + } + + // Throw if invalid Gitcoin contribution percentage + if (Number(this.gitcoinFactorRaw) < 0 || Number(this.gitcoinFactorRaw) > 99) { + throw new Error('Gitcoin contribution amount must be between 0% and 99%'); + } + + // Throw if there's negative values in the cart + this.donationInputs.forEach(donation => { + if (Number(donation.amount) < 0) { + throw new Error('Cannot have negative donation amounts'); + } + }); + + // If user has enough balance within Polygon, cost equals the minimum amount + let { isBalanceSufficient, requiredAmounts } = await this.hasEnoughBalanceInPolygon(); + + if (!isBalanceSufficient) { + this.polygon.showModal = true; + this.polygon.checkoutStatus = 'not-started'; + + this.user.requiredAmounts = objectMap(requiredAmounts, value => { + if (value.isBalanceSufficient == false) { + return value.amount; + } + }); + return; + } + + await this.setupPolygon(); + + // Token approvals and balance checks from bulk checkout contract + // (just checks data, does not execute approvals) + const allowanceData = await this.getAllowanceData( + ethereum.selectedAddress, bulkCheckoutAddressPolygon + ); + + // Save off cart data + this.polygon.checkoutStatus = 'pending'; + + if (allowanceData.length === 0) { + // Send transaction and exit function + await this.sendDonationTx(ethereum.selectedAddress); + return; + } + + // Request approvals then send donations --------------------------------------------------- + await this.requestAllowanceApprovalsThenExecuteCallback( + allowanceData, + ethereum.selectedAddress, + bulkCheckoutAddressPolygon, + this.sendDonationTx, + [ethereum.selectedAddress] + ); + + } catch (e) { + this.handleError(e); + } + }, + + async sendDonationTx(userAddress) { + // Get our donation inputs + const bulkTransaction = new web3.eth.Contract(bulkCheckoutAbi, bulkCheckoutAddressPolygon); + const donationInputsFiltered = this.getDonationInputs(); + + // Send transaction + bulkTransaction.methods + .donate(donationInputsFiltered) + .send({ from: userAddress, gas: this.polygon.estimatedGasCost, value: this.donationInputsNativeAmount }) + .on('transactionHash', async(txHash) => { + indicateMetamaskPopup(true); + console.log('Donation transaction hash: ', txHash); + _alert('Saving contributions. Please do not leave this page.', 'success', 2000); + await this.postToDatabase([txHash], bulkCheckoutAddressPolygon, userAddress); // Save contributions to database + await this.finalizeCheckout(); // Update UI and redirect + }) + .on('error', (error, receipt) => { + // If the transaction was rejected by the network with a receipt, the second parameter will be the receipt. + this.handleError(error); + }); + }, + + // Estimates the total gas cost of a polygon checkout and sends it to cart.js + async estimateGasCost() { + // The below heuristics are used instead of `estimateGas()` so we can send the donation + // transaction before the approval txs are confirmed, because if the approval txs + // are not confirmed then estimateGas will fail. + + return 70000; + }, + + // Returns true if user has enough balance within Polygon to avoid L1 deposit, false otherwise + async hasEnoughBalanceInPolygon() { + const requiredAmounts = {}; // keys are token symbols, values are required amounts as BigNumber + + // Get total amount needed for eack token by summing over donation inputs + this.donationInputs.forEach((donation) => { + const tokenSymbol = donation.name; + const amount = toBigNumber(donation.amount); + + if (!requiredAmounts[tokenSymbol]) { + // First time seeing this token, set the field and initial value + requiredAmounts[tokenSymbol] = { amount }; + } else { + // Increment total required amount of the token with new found value + requiredAmounts[tokenSymbol].amount = requiredAmounts[tokenSymbol].amount.add(amount); + } + }); + + // Compare amounts needed to balance + const web3 = this.initWeb3(); + const userAddress = ethereum.selectedAddress; + let isBalanceSufficient = true; + + for (let i = 0; i < this.cart.tokenList.length; i += 1) { + const tokenSymbol = this.cart.tokenList[i]; + + requiredAmounts[tokenSymbol].isBalanceSufficient = true; // initialize sufficiency result + const tokenDetails = this.getTokenByName(tokenSymbol); + + const userMaticBalance = toBigNumber(await web3.eth.getBalance(userAddress)); + const tokenIsMatic = tokenDetails.name === 'MATIC'; + + // Check user matic balance against required amount + if (userMaticBalance.lt(requiredAmounts[tokenSymbol].amount) && tokenIsMatic) { + requiredAmounts[tokenSymbol].isBalanceSufficient = false; + requiredAmounts[tokenSymbol].amount = parseFloat((( + requiredAmounts[tokenSymbol].amount - userMaticBalance + ) / 10 ** tokenDetails.decimals).toFixed(5)); + isBalanceSufficient = false; + } + + // Check if user has enough MATIC to cover gas costs + const gasFeeInWei = web3.utils.toWei( + (this.polygon.estimatedGasCost * 2).toString(), 'gwei' // using 2 gwei as gas price + ); + + if (userMaticBalance.lt(gasFeeInWei)) { + let requiredAmount = parseFloat(Number( + web3.utils.fromWei((gasFeeInWei - userMaticBalance).toString(), 'ether') + ).toFixed(5)); + + if (requiredAmounts['MATIC']) { + requiredAmounts['MATIC'].amount += requiredAmount; + } else { + requiredAmounts['MATIC'] = { + amount: requiredAmount, + isBalanceSufficient: false + }; + } + } + + // Check user token balance against required amount + const tokenContract = new web3.eth.Contract(token_abi, tokenDetails.addr); + const userTokenBalance = toBigNumber(await tokenContract.methods + .balanceOf(userAddress) + .call({ from: userAddress })); + + if (userTokenBalance.lt(requiredAmounts[tokenSymbol].amount)) { + requiredAmounts[tokenSymbol].isBalanceSufficient = false; + requiredAmounts[tokenSymbol].amount = parseFloat((( + requiredAmounts[tokenSymbol].amount - userTokenBalance + ) / 10 ** tokenDetails.decimals).toFixed(5)); + isBalanceSufficient = false; + } + } + + // Return result and required amounts + return { isBalanceSufficient, requiredAmounts }; + } + } +}); diff --git a/app/assets/v2/js/cart-ethereum-zksync.js b/app/assets/v2/js/cart-ethereum-zksync.js index 5ec2ddf1769..77b5b38c5a5 100644 --- a/app/assets/v2/js/cart-ethereum-zksync.js +++ b/app/assets/v2/js/cart-ethereum-zksync.js @@ -189,7 +189,7 @@ Vue.component('grantsCartEthereumZksync', { // Called on page load to initialize zkSync async setupZkSync() { - const network = this.network || 'mainnet'; // fallback to mainnet if no wallet is connected + const network = (this.network === 'testnet' ? null : this.network) || 'mainnet'; // fallback to mainnet if no wallet is connected if (!web3Modal || !provider) { return; // exit if web3 isn't defined, and we'll run this function later @@ -202,6 +202,7 @@ Vue.component('grantsCartEthereumZksync', { // alchemy: YOUR_ALCHEMY_API_KEY, // pocket: YOUR_POCKET_APPLICATION_KEY }); + this.zksync.checkoutManager = new ZkSyncCheckout.CheckoutManager(network); this.user.zksyncState = await this.zksync.checkoutManager.getState(this.user.address); }, @@ -236,7 +237,8 @@ Vue.component('grantsCartEthereumZksync', { await appCart.$refs.cart.postToDatabase( txHashes, // array of transaction hashes for each contribution this.zksync.contractAddress, // we use the zkSync mainnet contract address to represent zkSync deposits - this.user.address + this.user.address, + 'eth_zksync' ); this.zksync.checkoutStatus = 'complete'; // allows user to freely close tab now await appCart.$refs.cart.finalizeCheckout(); // Update UI and redirect @@ -266,6 +268,9 @@ Vue.component('grantsCartEthereumZksync', { // Estimates the total gas cost of a zkSync checkout and sends it to cart.js estimateGasCost() { + if (!this.user.zksyncState) + return; + // Estimate minimum gas cost based on 550 gas per transfer const gasPerTransfer = toBigNumber('550'); // may decrease as low as 340 as zkSync gets more traction const numberOfTransfers = String(this.donationInputs.length); diff --git a/app/assets/v2/js/cart-nav.js b/app/assets/v2/js/cart-nav.js new file mode 100644 index 00000000000..a708c40bda1 --- /dev/null +++ b/app/assets/v2/js/cart-nav.js @@ -0,0 +1,50 @@ +Vue.component('gc-cart-content', { + template: '#gc-cart-content', + delimiters: [ '[[', ']]' ], + data: () => { + + return { + items: [] + }; + }, + methods: { + init: function() { + // update items each time we open the dropdown + this.items = CartData.loadCart(); + }, + removeGrantFromCart: function(grant_id) { + // remove the item + CartData.removeIdFromCart(grant_id); + // refresh the data + this.items = CartData.loadCart(); + } + } +}); + +if (document.getElementById('gc-cart')) { + var app = new Vue({ + delimiters: [ '[[', ']]' ], + el: '#gc-cart', + data: { + cart_data_count: CartData.length() + }, + methods: { + updateCartCount: function(e) { + this.cart_data_count = e.detail.list.length || 0; + } + }, + mounted() { + // watch for cartUpdates + window.addEventListener('cartDataUpdated', this.updateCartCount); + }, + beforeDestroy() { + // unwatch cartUpdates + window.removeEventListener('cartDataUpdated', this.updateCartCount); + } + }); + + $(document).on('click', '.gc-cart .dropdown-menu', function(event) { + event.stopPropagation(); + }); +} + diff --git a/app/assets/v2/js/cart.js b/app/assets/v2/js/cart.js index 81256c957fd..6d58966d388 100644 --- a/app/assets/v2/js/cart.js +++ b/app/assets/v2/js/cart.js @@ -9,13 +9,27 @@ const { Zero: ZERO } = ethers.constants; const { BigNumber } = ethers; let appCart; -document.addEventListener('dataWalletReady', function(e) { +document.addEventListener('dataWalletReady', async function(e) { appCart.$refs['cart'].network = networkName; + appCart.$refs['cart'].sourceNetwork = networkName; + appCart.$refs['cart'].networkId = String(Number(web3.eth.currentProvider.chainId)); + if (appCart.$refs.cart.autoSwitchNetwork) { + try { + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: networkName == 'mainnet' ? '0x1' : '0x4' }] + }); // mainnet or rinkeby + appCart.$refs.cart.autoSwitchNetwork = false; + } catch (e) { + console.log(e); + } + } }, false); // needWalletConnection(); // Constants +const MATIC_ADDRESS = '0x0000000000000000000000000000000000001010'; const ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; const gitcoinAddress = '0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6'; // Gitcoin donation address for mainnet and rinkeby @@ -43,8 +57,12 @@ Vue.component('grants-cart', { { text: 'Wallet address', value: 'address' }, { text: 'Transaction Hash', value: 'txid' } ], + autoSwitchNetwork: true, + checkoutRecommendationIsCompleted: false, chainId: '', + networkId: '', network: 'mainnet', + sourceNetwork: 'mainnet', tabSelected: 'ETH', tabIndex: null, currentTokens: [], // list of all available tokens @@ -66,6 +84,8 @@ Vue.component('grants-cart', { // Checkout, zkSync zkSyncUnsupportedTokens: [], // Used to inform user which tokens in their cart are not on zkSync zkSyncEstimatedGasCost: undefined, // Used to tell user which checkout method is cheaper + polygonUnsupportedTokens: [], // Used to inform user which tokens in their cart are not on zkSync + polygonEstimatedGasCost: undefined, // Used to tell user which checkout method is cheaper isZkSyncDown: false, // disable zkSync when true isPolkadotExtInstalled: false, chainScripts: { @@ -171,6 +191,19 @@ Vue.component('grants-cart', { } return result; }, + filterByNetworkId: function() { + const vm = this; + let result; + + if (vm.networkId == '') { + result = vm.filterByChainId; + } else { + result = vm.filterByChainId.filter((item) => { + return String(item.networkId) === vm.networkId; + }); + } + return result; + }, // Returns true if user is logged in with GitHub, false otherwise isLoggedIn() { return document.contxt.github_handle; @@ -213,6 +246,8 @@ Vue.component('grants-cart', { // Array of objects containing all donations and associated data donationInputs() { + let isPolygon = this.nativeCurrency == 'MATIC'; + if (!this.grantsByTenant || this.tabSelected !== 'ETH') { return undefined; } @@ -220,13 +255,13 @@ Vue.component('grants-cart', { // Generate array of objects containing donation info from cart let gitcoinFactor = String(100 - (100 * this.gitcoinFactor)); const donations = this.grantsByTenant.map((grant, index) => { - const tokenDetails = this.getTokenByName(grant.grant_donation_currency); - const amount = parseUnits(String(grant.grant_donation_amount || 0), tokenDetails.decimals) + const tokenDetails = this.getTokenByName(grant.grant_donation_currency, isPolygon); + const amount = parseUnits(String(grant.grant_donation_amount || 0), tokenDetails?.decimals) .mul(gitcoinFactor) .div(100); return { - token: tokenDetails.addr, + token: tokenDetails?.addr, amount: amount.toString(), dest: grant.grant_admin_address, name: grant.grant_donation_currency, // token abbreviation, e.g. DAI @@ -238,8 +273,8 @@ Vue.component('grants-cart', { // Append the Gitcoin donations (these already account for gitcoinFactor) Object.keys(this.donationsToGitcoin).forEach((token) => { - const tokenDetails = this.getTokenByName(token); - const amount = parseUnits(String(this.donationsToGitcoin[token]), tokenDetails.decimals); + const tokenDetails = this.getTokenByName(token, isPolygon); + const amount = parseUnits(String(this.donationsToGitcoin[token]), tokenDetails?.decimals); const gitcoinGrantInfo = { // Manually fill this in so we can access it for the POST requests. @@ -265,7 +300,7 @@ Vue.component('grants-cart', { if (amount.gt(ZERO)) { donations.push({ amount: amount.toString(), - token: tokenDetails.addr, + token: tokenDetails?.addr, dest: gitcoinAddress, name: token, // token abbreviation, e.g. DAI grant: gitcoinGrantInfo, // equivalent to grant data from localStorage @@ -278,17 +313,24 @@ Vue.component('grants-cart', { return donations; }, - // Total amount of ETH that needs to be sent along with the transaction - donationInputsEthAmount() { - // Get the total ETH we need to send + // Total amount of native currency that needs to be sent along with the transaction + donationInputsNativeAmount() { + // Get the total native currency we need to send const initialValue = new BN('0'); - const ethAmountBN = this.donationInputs.reduce((accumulator, currentValue) => { - return currentValue.token === ETH_ADDRESS - ? accumulator.add(new BN(currentValue.amount)) // ETH donation + let nativeCurrencyAddress = this.nativeCurrency === 'MATIC' ? MATIC_ADDRESS : ETH_ADDRESS; + const nativeCurrencyAmountBN = this.donationInputs.reduce((accumulator, currentValue) => { + return currentValue.token === nativeCurrencyAddress + ? accumulator.add(new BN(currentValue.amount)) // native currency donation : accumulator.add(new BN('0')); // token donation }, initialValue); - return ethAmountBN.toString(10); + return nativeCurrencyAmountBN.toString(10); + }, + + nativeCurrency() { + let isPolygon = this.networkId === '80001' || this.networkId === '137'; + + return isPolygon ? 'MATIC' : 'ETH'; }, // Estimated gas limit for the transaction @@ -322,7 +364,7 @@ Vue.component('grants-cart', { // donation (i.e. one item in the cart). Because gas prices go down with batched // transactions, whereas this assumes they're constant, this gives us a conservative estimate const gasLimit = this.donationInputs.reduce((accumulator, currentValue) => { - const tokenAddr = currentValue.token.toLowerCase(); + const tokenAddr = currentValue.token?.toLowerCase(); if (currentValue.token === ETH_ADDRESS) { return accumulator + 70000; // ETH donation gas estimate @@ -351,19 +393,40 @@ Vue.component('grants-cart', { checkoutRecommendation() { const estimateL1 = Number(this.donationInputsGasLimitL1); // L1 gas cost estimate const estimateZkSync = Number(this.zkSyncEstimatedGasCost); // zkSync gas cost estimate + const estimatePolygon = Number(this.polygonEstimatedGasCost); // polygon gas cost estimate + + const exit = (recommendation) => { + this.checkoutRecommendationIsCompleted = true; + return recommendation; + }; - if (estimateL1 < estimateZkSync) { - const savingsInGas = estimateZkSync - estimateL1; - const savingsInPercent = Math.round(savingsInGas / estimateZkSync * 100); + const compareWithL2 = (estimateL2, name) => { + if (estimateL1 < estimateL2) { + const savingsInGas = estimateL2 - estimateL1; + const savingsInPercent = Math.round(savingsInGas / estimateL2 * 100); - return { name: 'Standard checkout', savingsInGas, savingsInPercent }; - } + return exit({ name: 'Standard checkout', savingsInGas, savingsInPercent }); + } - const savingsInGas = estimateL1 - estimateZkSync; - const percentSavings = savingsInGas / estimateL1 * 100; - const savingsInPercent = percentSavings > 99 ? 99 : Math.round(percentSavings); // max value of 99% + const savingsInGas = estimateL1 - estimateL2; + const percentSavings = savingsInGas / estimateL1 * 100; + const savingsInPercent = percentSavings > 99 ? 99 : Math.round(percentSavings); // max value of 99% + + return exit({ name, savingsInGas, savingsInPercent }); + }; - return { name: 'zkSync', savingsInGas, savingsInPercent }; + zkSyncComparisonResult = compareWithL2(estimateZkSync, 'zkSync'); + polygonComparisonResult = compareWithL2(estimatePolygon, 'Polygon'); + zkSyncSavings = zkSyncComparisonResult.name === 'zkSync' ? zkSyncComparisonResult.savingsInPercent : 0; + polygonSavings = polygonComparisonResult.name === 'Polygon' ? polygonComparisonResult.savingsInPercent : 0; + + if (zkSyncSavings > polygonSavings) { + return exit(zkSyncComparisonResult); + } else if (zkSyncSavings < polygonSavings) { + return exit(polygonComparisonResult); + } + + return exit(zkSyncComparisonResult); // recommendation will be standard checkout }, isHarmonyExtInstalled() { @@ -433,6 +496,11 @@ Vue.component('grants-cart', { this.zkSyncEstimatedGasCost = data.zkSyncEstimatedGasCost; }, + onPolygonUpdate: function(data) { + this.polygonUnsupportedTokens = data.polygonUnsupportedTokens; + this.polygonEstimatedGasCost = data.polygonEstimatedGasCost; + }, + tabChange: async function(input) { let vm = this; @@ -601,6 +669,11 @@ Vue.component('grants-cart', { copyToClipboard(CartData.share_url()); }, + updateCartData(e) { + this.grantData = (e && e.detail && e.detail.list && e.detail.list) || []; + update_cart_title(); + }, + removeGrantFromCart(id) { CartData.removeIdFromCart(id); this.grantData = CartData.loadCart(); @@ -738,8 +811,10 @@ Vue.component('grants-cart', { * response to facilitate backward compatibility * @param {String} name Token name, e.g. ETH or DAI */ - getTokenByName(name) { - if (name === 'ETH') { + getTokenByName(name, isPolygon = false) { + let token; + + if (name === 'ETH' && !isPolygon) { return { addr: ETH_ADDRESS, address: ETH_ADDRESS, @@ -748,8 +823,25 @@ Vue.component('grants-cart', { decimals: 18, priority: 1 }; + } else if (name === 'MATIC' && isPolygon) { + return { + addr: MATIC_ADDRESS, + address: MATIC_ADDRESS, + name: 'MATIC', + symbol: 'MATIC', + decimals: 18, + priority: 1 + }; } - return this.filterByChainId.filter(token => token.name === name)[0]; + + if (isPolygon) { + token = this.filterByChainId.filter(token => token.name === name && token.networkId == this.networkId)[0]; + return token; + } + + token = this.filterByChainId.filter(token => token.name === name)[0]; + + return token; }, async applyAmountToAllGrants(grant) { @@ -814,7 +906,7 @@ Vue.component('grants-cart', { * @param targetContract Address of the contract to check allowance against. Currently this * should only be the bulkCheckout contract address */ - async getAllowanceData(userAddress, targetContract) { + async getAllowanceData(userAddress, targetContract, isPolygon = false) { // Get list of tokens user is donating with const selectedTokens = Object.keys(this.donationsToGrants); @@ -828,22 +920,22 @@ Vue.component('grants-cart', { return this.donationInputs.reduce((accumulator, currentValue) => { return currentValue.token === tokenDetails.addr ? accumulator.add(new BN(currentValue.amount)) // token donation - : accumulator.add(new BN('0')); // ETH donation + : accumulator.add(new BN('0')); // native currency donation }, initialValue); }; // Loop over each token in the cart and check allowance for (let i = 0; i < selectedTokens.length; i += 1) { const tokenName = selectedTokens[i]; - const tokenDetails = this.getTokenByName(tokenName); + const tokenDetails = this.getTokenByName(tokenName, isPolygon); - // If ETH donation no approval is necessary, just check balance - if (tokenDetails.name === 'ETH') { + // If native currency donation no approval is necessary, just check balance + if (tokenDetails.name === this.nativeCurrency) { const userEthBalance = await web3.eth.getBalance(userAddress); - if (new BN(userEthBalance, 10).lt(new BN(this.donationInputsEthAmount, 10))) { + if (new BN(userEthBalance, 10).lt(new BN(this.donationInputsNativeAmount, 10))) { // User ETH balance is too small compared to selected donation amounts - throw new Error('Insufficient ETH balance to complete checkout'); + throw new Error(`Insufficient ${tokenDetails.name} balance to complete checkout`); } // ETH balance is sufficient, continue to next iteration since no approval check continue; @@ -1019,7 +1111,7 @@ Vue.component('grants-cart', { indicateMetamaskPopup(); bulkTransaction.methods .donate(donationInputsFiltered) - .send({ from: userAddress, gas: this.donationInputsGasLimitL1, value: this.donationInputsEthAmount }) + .send({ from: userAddress, gas: this.donationInputsGasLimitL1, value: this.donationInputsNativeAmount }) .on('transactionHash', async(txHash) => { console.log('Donation transaction hash: ', txHash); indicateMetamaskPopup(true); @@ -1034,23 +1126,20 @@ Vue.component('grants-cart', { }, // POSTs donation data to database. Wrapped in a try/catch, and if it fails, we fallback to the manual ingestion script - async postToDatabase(txHash, contractAddress, userAddress) { + async postToDatabase(txHash, contractAddress, userAddress, checkoutType = 'eth_std') { try { // this.grantsByTenant is the array used for donations, and this.donationInputs is computed from it // We loop through each donation to configure the payload then POST the required data const donations = this.donationInputs; const csrfmiddlewaretoken = document.querySelector('[name=csrfmiddlewaretoken]').value; - // All transactions are the same type, so if any hash begins with `sync-tx:` we know it's a zkSync checkout - const checkout_type = txHash[0].startsWith('sync') ? 'eth_zksync' : 'eth_std'; - // If standard checkout, stretch it so there's one hash for each donation (required for `for` loop below) - const txHashes = checkout_type === 'eth_zksync' ? txHash : new Array(donations.length).fill(txHash[0]); + const txHashes = checkoutType === 'eth_zksync' ? txHash : new Array(donations.length).fill(txHash[0]); // Configure template payload const saveSubscriptionPayload = { // Values that are constant for all donations - checkout_type, + checkoutType, contributor_address: userAddress, csrfmiddlewaretoken, frequency_count: 1, @@ -1094,11 +1183,19 @@ Vue.component('grants-cart', { const tokenName = donation.grant.grant_donation_currency; const tokenDetails = this.getTokenByName(tokenName); - // Gitcoin uses the zero address to represent ETH, but the contract does not. Therefore we - // get the value of denomination and token_address using the below logic instead of + // Gitcoin uses special addresses to represent native chain currencies, but the contract does not. + // Therefore we get the value of denomination and token_address using the below logic instead of // using tokenDetails.addr - const isEth = tokenName === 'ETH'; - const tokenAddress = isEth ? '0x0000000000000000000000000000000000000000' : tokenDetails.addr; + switch (tokenName) { + case 'ETH' && checkoutType !== 'eth_polygon': + tokenAddress = '0x0000000000000000000000000000000000000000'; + break; + case 'MATIC' && checkoutType === 'eth_polygon': + tokenAddress = '0x0000000000000000000000000000000000001010'; + break; + default: + tokenAddress = tokenDetails.addr; + } // Replace undefined comments with empty strings const comment = donation.grant.grant_comments === undefined ? '' : donation.grant.grant_comments; @@ -1161,18 +1258,17 @@ Vue.component('grants-cart', { // Something went wrong, so we use the manual ingestion process instead console.error(err); console.log('Standard contribution ingestion failed, falling back to manual ingestion'); - await this.postToDatabaseManualIngestion(txHash, userAddress); + await this.postToDatabaseManualIngestion(txHash, userAddress, checkoutType); } }, // Alternative to postToDatabase that uses the manual ingestion process - async postToDatabaseManualIngestion(txHash, userAddress) { + async postToDatabaseManualIngestion(txHash, userAddress, checkoutType = 'eth_std') { // Determine if this was a zkSync checkout or standard L1 checkout. For this endpoint, we pass a txHash // to ingest L1 contributions or an address to pass L2 contributions - const checkout_type = txHash[0].startsWith('sync') ? 'eth_zksync' : 'eth_std'; - txHash = checkout_type === 'eth_std' ? txHash[0] : ''; // txHash is always an array of hashes from checkout - userAddress = checkout_type === 'eth_zksync' ? userAddress : ''; + txHash = checkoutType === 'eth_std' || checkoutType === 'eth_polygon' ? txHash[0] : ''; // txHash is always an array of hashes from checkout + userAddress = checkoutType === 'eth_zksync' ? userAddress : ''; // Get user's signature to prevent ingesting arbitrary transactions under your own username, then ingest const { signature, message } = await this.signMessage(userAddress); @@ -1204,14 +1300,13 @@ Vue.component('grants-cart', { } }, - // Asks user to sign a message as verification they own the provided address async signMessage(userAddress) { const baseMessage = 'Something went wrong, but we want to ensure your contributions are counted!\n\nSign this message as verification that you control the provided wallet address so we can process your contributions'; // base message that will be signed const ethersProvider = new ethers.providers.Web3Provider(provider); // ethers provider instance const signer = ethersProvider.getSigner(); // ethers signers const { chainId } = await ethersProvider.getNetwork(); // append chain ID if not mainnet to mitigate replay attack - const message = chainId === 1 ? baseMessage : `${baseMessage}\n\nChain ID: ${chainId}`; + const message = chainId === 1 || chainId === 137 ? baseMessage : `${baseMessage}\n\nChain ID: ${chainId}`; // Get signature from user const isValidSignature = (sig) => ethers.utils.isHexString(sig) && sig.length === 132; // used to verify signature @@ -1238,7 +1333,7 @@ Vue.component('grants-cart', { * success alert */ async finalizeCheckout() { - // Number of items descides the timeout time + // Number of items decides the timeout time const timeout_amount = 1500 + (this.grantsByTenant.length * 500); // Clear cart, redirect back to grants page, and show success alert @@ -1501,11 +1596,16 @@ Vue.component('grants-cart', { // Support responsive design window.addEventListener('resize', this.onResize); + // watch for cartUpdates + window.addEventListener('cartDataUpdated', this.updateCartData); + // Show user cart now this.isLoading = false; }, beforeDestroy() { + // unwatch cartUpdates + window.removeEventListener('cartDataUpdated', this.updateCartData); window.removeEventListener('resize', this.onResize); } }); diff --git a/app/assets/v2/js/data-chains.js b/app/assets/v2/js/data-chains.js index d64e34229c7..576473ffefc 100644 --- a/app/assets/v2/js/data-chains.js +++ b/app/assets/v2/js/data-chains.js @@ -1254,6 +1254,46 @@ var dataChains = ], "infoURL": "https://poa.network" + }, + { + "name": "Polygon(Matic) Testnet Mumbai", + "chainId": 80001, + "shortName": "maticmum", + "chain": "Polygon(Matic)", + "network": "testnet", + "networkId": 80001, + "rpc": [ + "https://rpc-mumbai.maticvigil.com" + ], + "faucets": [ + "https://faucet.matic.network/" + ], + "nativeCurrency": { + "name": "Matic", + "symbol": "MATIC", + "decimals": 18 + }, + "infoURL":"https://matic.network/" + }, + { + "name": "Polygon(Matic) Mainnet", + "chainId": 137, + "shortName": "matic", + "chain": "Polygon(Matic)", + "network": "mainnet", + "networkId": 137, + "rpc": [ + "https://rpc-mainnet.maticvigil.com", + "https://rpc-mainnet.matic.network", + "https://rpc-mainnet.matic.quiknode.pro", + "https://matic-mainnet.chainstacklabs.com" + ], + "nativeCurrency": { + "name": "Matic", + "symbol": "MATIC", + "decimals": 18 + }, + "infoURL":"https://matic.network/" } ]; diff --git a/app/assets/v2/js/grants/_detail-component.js b/app/assets/v2/js/grants/_detail-component.js index 11735236a4b..ec11d618604 100644 --- a/app/assets/v2/js/grants/_detail-component.js +++ b/app/assets/v2/js/grants/_detail-component.js @@ -11,11 +11,18 @@ Quill.register('modules/ImageExtend', ImageExtend); Vue.mixin({ methods: { + updateCartData: function(e) { + const vm = this; + const grants_in_cart = (e && e.detail && e.detail.list && e.detail.list) || []; + const isInCart = grants_in_cart.find((grant) => vm.grant.id == grant.grant_id); + + vm.$set(vm.grant, 'isInCart', isInCart); + }, grantInCart: function() { const vm = this; - const inCart = CartData.cartContainsGrantWithId(vm.grant.id); + const isInCart = CartData.cartContainsGrantWithId(vm.grant.id); - vm.$set(vm.grant, 'isInCart', inCart); + vm.$set(vm.grant, 'isInCart', isInCart); return vm.grant.isInCart; }, addToCart: async function() { @@ -25,20 +32,12 @@ Vue.mixin({ vm.$set(vm.grant, 'isInCart', true); CartData.addToCart(response.grant); - if (typeof showSideCart != 'undefined') { - showSideCart(); - } - }, removeFromCart: function() { const vm = this; vm.$set(vm.grant, 'isInCart', false); CartData.removeIdFromCart(vm.grant.id); - if (typeof showSideCart != 'undefined') { - showSideCart(); - } - }, editGrantModal: function() { const vm = this; @@ -84,7 +83,8 @@ Vue.mixin({ 'rsk_payout_address': vm.grant.rsk_payout_address, 'algorand_payout_address': vm.grant.algorand_payout_address, 'region': vm.grant.region?.name || undefined, - 'has_external_funding': vm.grant.has_external_funding + 'has_external_funding': vm.grant.has_external_funding, + 'grant_tags[]': JSON.stringify(vm.grantTagsFormatted) }; if (vm.logo) { @@ -428,6 +428,24 @@ Vue.mixin({ this.grant.team_members = value; } }, + grantTagsFormatted: { + get() { + return this.grant.grant_tags.map((grant_tag)=> { + if (!grant_tag?.fields) { + return grant_tag; + } + + return { + 'id': grant_tag.pk, + 'name': grant_tag.fields.name + }; + }); + + }, + set(value) { + this.grant.grant_tags = value; + } + }, editor() { if (!this.$refs.myQuillEditor) { return; @@ -470,6 +488,7 @@ Vue.mixin({ } } } + }); @@ -546,6 +565,7 @@ Vue.component('grant-details', { { 'name': 'east_asia', 'label': 'East Asia'}, { 'name': 'southeast_asia', 'label': 'Southeast Asia'} ], + grant_tags: document.grant_tags, externalFundingOptions: [ {'key': 'yes', 'value': 'Yes, this project has raised external funding.'}, {'key': 'no', 'value': 'No, this project has not raised external funding.'} @@ -561,6 +581,15 @@ Vue.component('grant-details', { vm.editor.updateContents(JSON.parse(vm.grant.description_rich)); } vm.grantInCart(); + + // watch for cartUpdates + window.addEventListener('cartDataUpdated', vm.updateCartData); + }, + beforeDestroy() { + const vm = this; + + // unwatch cartUpdates + window.removeEventListener('cartDataUpdated', vm.updateCartData); }, watch: { grant: { diff --git a/app/assets/v2/js/grants/_detail.js b/app/assets/v2/js/grants/_detail.js index 9f9acf633df..28df3584323 100644 --- a/app/assets/v2/js/grants/_detail.js +++ b/app/assets/v2/js/grants/_detail.js @@ -49,10 +49,10 @@ Vue.mixin({ switch (vm.tab) { case 'sybil_profile': - vm.tabSelected = 3; + vm.tabSelected = 4; break; case 'stats': - vm.tabSelected = 4; + vm.tabSelected = 3; break; default: vm.tabSelected = 0; @@ -123,8 +123,8 @@ Vue.mixin({ }, backNavigation: function() { const vm = this; - const lgi = localStorage.getItem('last_grants_index'); - const lgt = localStorage.getItem('last_grants_title'); + const lgt = localStorage.getItem('last_grants_title') || 'Grants'; + const lgi = document.referrer.indexOf(location.host) != -1 ? 'javascript:history.back()' : '/grants/explorer'; if (lgi && lgt) { vm.$set(vm.backLink, 'url', lgi); diff --git a/app/assets/v2/js/grants/_new.js b/app/assets/v2/js/grants/_new.js index ae19d88c9f5..5de934f5b82 100644 --- a/app/assets/v2/js/grants/_new.js +++ b/app/assets/v2/js/grants/_new.js @@ -107,6 +107,8 @@ Vue.mixin({ vm.$set(vm.errors, 'eth_payout_address', 'Please enter ETH address'); } else if (vm.form.eth_payout_address.trim().endsWith('.eth')) { vm.$set(vm.errors, 'eth_payout_address', 'ENS is not supported. Please enter ETH address'); + } else if (!web3.utils.isAddress(vm.form.eth_payout_address)) { + vm.$set(vm.errors, 'eth_payout_address', 'Please enter a valid ETH address'); } } else if ( vm.chainId == 'zcash' && @@ -134,8 +136,8 @@ Vue.mixin({ if (!vm.form.grant_type) { vm.$set(vm.errors, 'grant_type', 'Please select the grant category'); } - if (!vm.form.grant_categories.length > 0) { - vm.$set(vm.errors, 'grant_categories', 'Please one or more grant subcategory'); + if (!vm.form.grant_tags.length > 0) { + vm.$set(vm.errors, 'grant_tags', 'Please select one or more grant tag'); } if (vm.form.description_rich.length < 10) { vm.$set(vm.errors, 'description', 'Please enter description for the grant'); @@ -184,7 +186,7 @@ Vue.mixin({ 'rsk_payout_address': form.rsk_payout_address, 'algorand_payout_address': form.algorand_payout_address, 'grant_type': form.grant_type, - 'categories[]': form.grant_categories, + 'tags[]': form.grant_tags, 'network': form.network, 'region': form.region, 'has_external_funding': form.has_external_funding @@ -247,13 +249,6 @@ Vue.mixin({ } }); }, - type_to_category_mapping: function() { - let vm = this; - - let grant_type = this.grant_types.filter(grant_type => grant_type.name == vm.form.grant_type); - - return grant_type[0].categories; - }, onFileChange(e) { let vm = this; @@ -339,6 +334,7 @@ if (document.getElementById('gc-new-grant')) { return { chainId: '', grant_types: document.grant_types, + grant_tags: document.grant_tags, grant_regions: grant_regions, externalFundingOptions: externalFundingOptions, usersOptions: [], @@ -368,7 +364,7 @@ if (document.getElementById('gc-new-grant')) { rsk_payout_address: '', algorand_payout_address: '', grant_type: '', - grant_categories: [], + grant_tags: [], network: 'mainnet' }, editorOptionPrio: { @@ -435,7 +431,7 @@ if (document.getElementById('gc-new-grant')) { 'eth_payout_address', 'grant_type', 'team_members', - 'grant_categories' + 'grant_tags' ]; for (const key of writeToRoot) { diff --git a/app/assets/v2/js/grants/components.js b/app/assets/v2/js/grants/components.js index 46f72687834..a804d77776a 100644 --- a/app/assets/v2/js/grants/components.js +++ b/app/assets/v2/js/grants/components.js @@ -84,14 +84,12 @@ Vue.component('grant-card', { vm.$set(vm.grant, 'isInCart', true); CartData.addToCart(response.grant); - showSideCart(); }, removeFromCart: function() { let vm = this; vm.$set(vm.grant, 'isInCart', false); CartData.removeIdFromCart(vm.grant.id); - showSideCart(); }, addToCollection: async function(collection, grant) { const collectionAddGrantURL = `/grants/v1/api/collections/${collection.id}/grants/add`; @@ -145,11 +143,9 @@ Vue.component('grant-collection', { (collection.grants || []).forEach((grant) => { CartData.addToCart(grant); }); - - showSideCart(); }, getGrantLogo(index) { - return this.collection.grants[index].logo; + return `${this.collection.cache?.grants[index]?.logo}`; } } }); diff --git a/app/assets/v2/js/grants/create-collection-modal.js b/app/assets/v2/js/grants/create-collection-modal.js index dc5f4ae7f72..eea4a5242a8 100644 --- a/app/assets/v2/js/grants/create-collection-modal.js +++ b/app/assets/v2/js/grants/create-collection-modal.js @@ -66,7 +66,7 @@ Vue.component('create-collection-modal', { try { response = await fetchData('/grants/v1/api/collections/new', 'POST', body, {'X-CSRFToken': csrfmiddlewaretoken}); - const redirect = `/grants/explorer/collections?collection_id=${response.collection.id}`; + const redirect = `/grants/explorer/?collection_id=${response.collection.id}`; _alert('Congratulations, your new collection was created successfully!', 'success'); diff --git a/app/assets/v2/js/grants/funding.js b/app/assets/v2/js/grants/funding.js index 938bab1b2dd..d6c6b0b3d60 100644 --- a/app/assets/v2/js/grants/funding.js +++ b/app/assets/v2/js/grants/funding.js @@ -17,8 +17,6 @@ $(document).ready(function() { // const formData = objectifySerialized($(this).serializeArray()); CartData.addToCart(grantDetails); - - showSideCart(); }); $('.infinite-container').on( @@ -30,263 +28,6 @@ $(document).ready(function() { const formData = objectifySerialized($(this).serializeArray()); CartData.addToCart(formData); - - showSideCart(); } ); - - $('#close-side-cart').click(function() { - hideSideCart(); - }); - - $('#side-cart-data').on('click', '#apply-to-all', async function() { - // Get preferred cart data - let cartData = CartData.loadCart(); - const network = document.web3network || 'mainnet'; - const selected_grant_index = $(this).data('id'); - const preferredAmount = - cartData[selected_grant_index].grant_donation_amount; - const preferredTokenName = - cartData[selected_grant_index].grant_donation_currency; - const preferredTokenAddress = tokens(network) - .filter((token) => token.name === preferredTokenName) - .map((token) => token.addr)[selected_grant_index]; - - // Get fallback amount in ETH (used when token is not available for a grant) - const url = `${window.location.origin}/sync/get_amount?amount=${preferredAmount}&denomination=${preferredTokenName}`; - const response = await fetch(url); - const fallbackAmount = (await response.json())[0].eth; - - // Update cart values - cartData.forEach((grant, index) => { - const acceptsAllTokens = - grant.grant_token_address === - '0x0000000000000000000000000000000000000000'; - const acceptsSelectedToken = - grant.grant_token_address === preferredTokenAddress; - - if (acceptsAllTokens || acceptsSelectedToken) { - // Use the user selected option - cartData[index].grant_donation_amount = preferredAmount; - cartData[index].grant_donation_currency = preferredTokenName; - } else { - // If the selected token is not available, fallback to ETH - cartData[index].grant_donation_amount = fallbackAmount; - cartData[index].grant_donation_currency = 'ETH'; - } - }); // end cartData.forEach - - // Update cart - CartData.setCart(cartData); - showSideCart(); - }); }); - -// HELPERS - -function sideCartRowForGrant(grant, index) { - let cartRow = ` -
-
-
- Grant logo -
-
- ${grant.grant_title} -
-
- -
-
-
-
-
- -
-
- -
-
- -
- `; - - return cartRow; -} - -function tokenOptionsForGrant(grant) { - var network = document.web3network; - - if (!network) { - network = 'mainnet'; - } - - // let tokenDataList = tokens(network); - let tokenDataList = allTokens.filter( - (token) => token.network === networkName || 'mainnet' - ); - let tokenDefault = 'ETH'; - - if (grant.tenants && grant.tenants.includes('ZCASH')) { - tokenDataList = tokenDataList.filter((token) => token.chainId === 123123); - tokenDefault = 'ZEC'; - } else if (grant.tenants && grant.tenants.includes('CELO')) { - tokenDataList = tokenDataList.filter((token) => token.chainId === 42220); - tokenDefault = 'CELO'; - } else if (grant.tenants && grant.tenants.includes('ZIL')) { - tokenDataList = tokenDataList.filter((token) => token.chainId === 102); - tokenDefault = 'ZIL'; - } else if (grant.tenants && grant.tenants.includes('HARMONY')) { - tokenDataList = tokenDataList.filter((token) => token.chainId === 1000); - tokenDefault = 'ONE'; - } else if (grant.tenants && grant.tenants.includes('BINANCE')) { - tokenDataList = tokenDataList.filter((token) => token.chainId === 56); - tokenDefault = 'BNB'; - } else if (grant.tenants && grant.tenants.includes('POLKADOT')) { - tokenDataList = tokenDataList.filter((token) => token.chainId === 58); - tokenDefault = 'DOT'; - } else if (grant.tenants && grant.tenants.includes('KUSAMA')) { - tokenDataList = tokenDataList.filter((token) => token.chainId === 59); - tokenDefault = 'KSM'; - } else if (grant.tenants && grant.tenants.includes('RSK')) { - tokenDataList = tokenDataList.filter(token => token.chainId === 30); - tokenDefault = 'RBTC'; - } else if (grant.tenants && grant.tenants.includes('ALGORAND')) { - tokenDataList = tokenDataList.filter(token => token.chainId === 1001); - tokenDefault = 'ALGO'; - } else { - tokenDataList = tokenDataList.filter((token) => token.chainId === 1); - } - - const acceptsAllTokens = - grant.grant_token_address === - '0x0000000000000000000000000000000000000000' || - grant.grant_token_address === '0x0'; - - let options = ''; - - if (!acceptsAllTokens) { - options += ` - - `; - - tokenDataList = tokenDataList.filter((tokenData) => { - return tokenData.address === grant.grant_token_address; - }); - } - - for (let index = 0; index < tokenDataList.length; index++) { - const tokenData = tokenDataList[index]; - - if (tokenData.divider) { - options += ` - - `; - } else { - options += ` - - `; - } - } - - return options; -} - -function showSideCart() { - // Remove elements in side cart - $('#side-cart-data').find('div.side-cart-row').remove(); - - // Add all elements in side cart - let cartData = CartData.loadCart(); - - cartData.forEach((grant, index) => { - const cartRowHtml = sideCartRowForGrant(grant, index); - - $('#side-cart-data').append(cartRowHtml); - - // Register remove click handler - $(`#side-cart-row-remove-${grant.grant_id}`).click(function() { - if (typeof appGrants !== 'undefined') { - appGrants.grants.filter((grantSingle) => { - if (Number(grantSingle.id) === Number(grant.grant_id)) { - grantSingle.isInCart = false; - } - }); - } else if ( - typeof appGrantDetails !== 'undefined' && - appGrantDetails.grant.id === Number(grant.grant_id) - ) { - appGrantDetails.grant.isInCart = false; - } - - $(`#side-cart-row-${grant.grant_id}`).remove(); - CartData.removeIdFromCart(grant.grant_id); - }); - - // Register change amount handler - $(`#side-cart-amount-${grant.grant_id}`).change(function() { - const newAmount = parseFloat($(this).val()); - - CartData.updateCartItem( - grant.grant_id, - 'grant_donation_amount', - newAmount - ); - }); - - // Select appropriate currency - $(`#side-cart-currency-${grant.grant_id}`).val( - grant.grant_donation_currency - ); - - // Register currency change handler - $(`#side-cart-currency-${grant.grant_id}`).change(function() { - CartData.updateCartItem( - grant.grant_id, - 'grant_donation_currency', - $(this).val() - ); - }); - - $(`#side-cart-currency-${grant.grant_id}`).select2(); - }); - - const isShowing = $('#side-cart').hasClass('col-12'); - - if (!isShowing) { - toggleSideCart(); - } - - // Scroll To top on mobile - if (window.innerWidth < 768) { - const cartTop = $('#side-cart').position().top; - - window.scrollTo(0, cartTop); - } -} - -function hideSideCart() { - const isShowing = $('#side-cart').hasClass('col-12'); - - if (!isShowing) { - return; - } - - toggleSideCart(); -} - -function toggleSideCart() { - $('#grants-details > div').toggleClass( - 'col-12 col-md-8 col-lg-9 d-none d-md-block side-cart-open' - ); - - $('#side-cart').toggle(); - $('#side-cart').toggleClass('col-12 col-md-4 col-lg-3'); - $('#funding-card').toggleClass('mr-md-5 mr-md-3 d-none d-lg-block'); -} diff --git a/app/assets/v2/js/grants/index.js b/app/assets/v2/js/grants/index.js index f209118bade..2f7bd781bba 100644 --- a/app/assets/v2/js/grants/index.js +++ b/app/assets/v2/js/grants/index.js @@ -5,10 +5,8 @@ let numGrants = ''; $(document).ready(() => { - if ($('.grants_type_nav').length) { - localStorage.setItem('last_grants_index', document.location.href); - localStorage.setItem('last_grants_title', $('title').text().split('|')[0]); - } + localStorage.setItem('last_grants_title', $('title').text().split('|')[0]); + if (document.location.href.indexOf('/cart') == -1) { localStorage.setItem('last_all_grants_index', document.location.href); localStorage.setItem('last_all_grants_title', $('title').text().split('|')[0]); @@ -26,92 +24,152 @@ $(document).ready(() => { // toggleStyle(document.current_style); }); -Vue.component('grant-sidebar', { - props: [ - 'filter_grants', 'grant_types', 'type', 'selected_category', 'keyword', 'following', 'set_type', - 'idle_grants', 'show_contributions', 'query_params', 'round_num', 'sub_round_slug', 'customer_name', - 'featured' - ], - data: function() { - return { - search: this.keyword, - show_filters: false, - handle: document.contxt.github_handle - }; - }, - methods: { - toggleFollowing: function(state, event) { - event.preventDefault; - this.filter_grants({following: state}); - }, - toggleIdle: function(state, event) { - event.preventDefault; - this.filter_grants({idle_grants: state}); - }, - toggleContributionView: function(state, event) { - event.preventDefault; - this.filter_grants({show_contributions: state}); - }, - toggleMyGrants: function(state, event) { - let me = state ? 'me' : 'all'; - - event.preventDefault; - this.filter_grants({type: me, category: '', keyword: ''}); - }, - isMobileDevice: function() { - return window.innerWidth < 576; - }, - toggleMyCollections: function(state, event) { - let me = state ? {type: 'collections', keyword: this.handle} : {type: 'all', keyword: ''}; - - this.filter_grants(me); - - this.search = me.keyword; - }, - filterLink: function(params) { - - return this.filter_grants(params); - }, - searchKeyword: function() { - if (this.timeout) { - clearTimeout(this.timeout); - } - - this.timeout = setTimeout(() => { - this.filter_grants({keyword: this.search}); - }, 1000); - }, - onResize: function() { - if (!this.isMobileDevice() && this.show_filters !== null) { - this.show_filters = null; - } else if (this.isMobileDevice() && this.show_filters === null) { - this.show_filters = false; - } - } - }, - mounted() { - window.addEventListener('resize', this.onResize); - } -}); +// Vue.component('grant-sidebar', { +// props: [ +// 'filter_grants', 'grant_types', 'type', 'selected_category', 'keyword', 'following', 'set_type', +// 'idle_grants', 'show_contributions', 'query_params', 'round_num', 'sub_round_slug', 'customer_name', +// 'featured' +// ], +// data: function() { +// return { +// search: this.keyword, +// show_filters: false, +// handle: document.contxt.github_handle +// }; +// }, +// methods: { +// toggleFollowing: function(state, event) { +// event.preventDefault; +// this.filter_grants({following: state}); +// }, +// toggleIdle: function(state, event) { +// event.preventDefault; +// this.filter_grants({idle_grants: state}); +// }, +// toggleContributionView: function(state, event) { +// event.preventDefault; +// this.filter_grants({show_contributions: state}); +// }, +// toggleMyGrants: function(state, event) { +// let me = state ? 'me' : 'all'; + +// event.preventDefault; +// this.filter_grants({type: me, category: '', keyword: ''}); +// }, +// isMobileDevice: function() { +// return window.innerWidth < 576; +// }, +// toggleMyCollections: function(state, event) { +// let me = state ? {type: 'collections', keyword: this.handle} : {type: 'all', keyword: ''}; + +// this.filter_grants(me); + +// this.search = me.keyword; +// }, +// filterLink: function(params) { + +// return this.filter_grants(params); +// }, +// searchKeyword: function() { +// if (this.timeout) { +// clearTimeout(this.timeout); +// } + +// this.timeout = setTimeout(() => { +// this.filter_grants({keyword: this.search}); +// }, 1000); +// }, +// onResize: function() { +// if (!this.isMobileDevice() && this.show_filters !== null) { +// this.show_filters = null; +// } else if (this.isMobileDevice() && this.show_filters === null) { +// this.show_filters = false; +// } +// } +// }, +// mounted() { +// window.addEventListener('resize', this.onResize); +// } +// }); if (document.getElementById('grants-showcase')) { + const baseParams = { + page: 1, + limit: 6, + me: false, + sort_option: 'weighted_shuffle', + network: 'mainnet', + // keyword: this.keyword, + state: 'active', + profile: false, + sub_round_slug: false, + collections_page: 1, + grant_regions: [], + grant_types: [], + grant_tags: [], + tenants: [], + idle: true + + }; + + const grantRegions = [ + { 'name': 'north_america', 'label': 'North America'}, + { 'name': 'oceania', 'label': 'Oceania'}, + { 'name': 'latin_america', 'label': 'Latin America'}, + { 'name': 'europe', 'label': 'Europe'}, + { 'name': 'africa', 'label': 'Africa'}, + { 'name': 'middle_east', 'label': 'Middle East'}, + { 'name': 'india', 'label': 'India'}, + { 'name': 'east_asia', 'label': 'East Asia'}, + { 'name': 'southeast_asia', 'label': 'Southeast Asia'} + ]; + + const grantTenants = [ + {'name': 'ETH', 'label': 'Eth'}, + {'name': 'ZCASH', 'label': 'Zcash'}, + {'name': 'ZIL', 'label': 'Zil'}, + {'name': 'CELO', 'label': 'Celo'}, + {'name': 'POLKADOT', 'label': 'Polkadot'}, + {'name': 'HARMONY', 'label': 'Harmony'}, + {'name': 'KUSAMA', 'label': 'Kusama'}, + {'name': 'BINANCE', 'label': 'Binance'}, + {'name': 'RSK', 'label': 'Rsk'}, + {'name': 'ALGORAND', 'label': 'Algorand'} + ]; + + // const grant_tags = [ + // {'name': 'ETH', 'label': 'Eth'}, + // {'name': 'ZCASH', 'label': 'Zcash'}, + // {'name': 'ZIL', 'label': 'Zil'}, + // {'name': 'CELO', 'label': 'Celo'}, + // {'name': 'POLKADOT', 'label': 'Polkadot'}, + // {'name': 'HARMONY', 'label': 'Harmony'}, + // {'name': 'KUSAMA', 'label': 'Kusama'}, + // {'name': 'BINANCE', 'label': 'Binance'}, + // {'name': 'RSK', 'label': 'Rsk'}, + // {'name': 'ALGORAND', 'label': 'Algorand'} + // ]; + + + // let sort = getParam('sort'); + + // if (!sort) { + // sort = 'weighted_shuffle'; + // } - let sort = getParam('sort'); - - if (!sort) { - sort = 'weighted_shuffle'; - } var appGrants = new Vue({ delimiters: [ '[[', ']]' ], el: '#grants-showcase', data: { activePage: document.activePage, grants: [], + clrData: {}, + grantRegions: grantRegions, + grantTenants: grantTenants, + grant_tags: [], grant: {}, - page: 1, - collectionsPage: 1, - limit: 6, + collectionsPage: null, + cart_data_count: CartData.length(), show_active_clrs: window.localStorage.getItem('show_active_clrs') != 'false', - sort: sort, network: document.network, keyword: document.keyword, current_type: document.current_type, @@ -129,17 +187,29 @@ if (document.getElementById('grants-showcase')) { view: localStorage.getItem('grants_view') || 'grid', shortView: true, bottom: false, + sub_round_slug: false, cart_lock: false, collection_id: document.collection_id, - round_num: document.round_num, - clr_round_pk: document.clr_round_pk, - sub_round_slug: document.sub_round_slug, - customer_name: document.customer_name, + // round_num: document.round_num, + // clr_round_pk: document.clr_round_pk, + // sub_round_slug: document.sub_round_slug, + // customer_name: document.customer_name, activeCollection: null, grantsNumPages, grantsHasNext, numGrants, - regex_style: {} + regex_style: {}, + params: Object.assign({}, baseParams), + tagsOptions: [], + tabIndex: null, + tabSelected: undefined, + loadingCollections: false, + searchVisible: false, + searchParams: undefined, + observer: null, + observed: null, + sticky_active: false, + fetchedPages: [] }, methods: { toggleStyle: function(style) { @@ -175,233 +245,125 @@ if (document.getElementById('grants-showcase')) { localStorage.setItem('grants_view', mode); this.view = mode; }, - setCurrentType: function(currentType) { - this.current_type = currentType; + fetchClrGrants: async function() { + let vm = this; + let url = '/api/v0.1/grants_clr/'; + let getClr = await fetch(url); + let clrJson = await getClr.json(); - if (this.current_type === 'collections') { - this.clearSingleCollection(); - } + vm.clrData = clrJson; + }, - this.updateURI(); + changeBanner: function() { + this.regex_style = document.all_routing_policies && + document.all_routing_policies.find(policy => { + return new RegExp(policy.url_pattern).test(window.location.href); + }); + this.toggleStyle(this.regex_style || document.current_style); }, - updateURI: function() { + resetFilters: function() { let vm = this; - const q = vm.getQueryParams(); - - if (vm.round_num) { - let uri = `/grants/clr/${vm.round_num}/`; - if (vm.sub_round_slug && !vm.customer_name) { - uri = `/grants/clr/${vm.round_num}/${vm.sub_round_slug}/`; - } + vm.params = Object.assign({}, baseParams); + vm.fetchedPages = []; + vm.fetchGrants(); - if (!vm.sub_round_slug && vm.customer_name) { - uri = `/grants/clr/${vm.customer_name}/${vm.round_num}/`; - } + }, + changeQuery: function(query) { + let vm = this; - if (vm.sub_round_slug && vm.customer_name) { - uri = `/grants/clr/${vm.customer_name}/${vm.round_num}/${vm.sub_round_slug}/`; - } + vm.fetchedPages = []; + vm.$set(vm, 'params', {...vm.params, ...query}); - if (this.current_type === 'all') { - window.history.pushState('', '', `${uri}?${q || ''}`); - } else { - window.history.pushState('', '', `${uri}?type=${this.current_type}&${q || ''}`); - } + if (vm.tabSelected === 'grants') { + vm.fetchGrants(); } else { - let uri = '/grants/explorer/'; + vm.updateUrlParams(); - if (this.current_type === 'all') { - window.history.pushState('', '', `${uri}?${q || ''}`); - } else { - window.history.pushState('', '', `${uri}${this.current_type}?${q || ''}`); - } } + }, + filterCollection: function(collectionId) { + let vm = this; - if (this.current_type === 'activity') { - const triggerTS = function() { - const activeElement = $('.infinite-more-link'); - - if (activeElement.length) { - $('.infinite-more-link').click(); - } else { - setTimeout(triggerTS, 1000); - } - }; + vm.params = Object.assign({}, baseParams); - setTimeout(triggerTS, 1000); - } + vm.changeQuery({collection_id: collectionId}); + vm.tabIndex = 0; }, - getQueryParams: function() { - const query_elements = {}; + getUrlParams: function() { + let vm = this; - if (this.category && this.current_type !== 'all') { - query_elements['category'] = this.category; - } + const url = new URL(location.href); + const params = new URLSearchParams(url.search); - if (this.keyword) { - query_elements['keyword'] = this.keyword; - } - if (this.idle_grants) { - query_elements['idle'] = this.idle_grants; - } - if (this.following) { - query_elements['following'] = this.following; - } - if (this.show_contributions) { - query_elements['only_contributions'] = this.show_contributions; - } - if (this.featured) { - query_elements['featured'] = this.featured; - } - if (this.sort !== 'weighted_shuffle') { - query_elements['sort'] = this.sort; - } - if (this.network !== 'mainnet') { - query_elements['network'] = this.network; - } - if (this.current_type === 'collections') { - if (this.collection_id) { - query_elements['collection_id'] = this.collection_id; - } - } + const param_is_array = [ 'grant_regions', 'tenants', 'grant_types', 'grant_tags' ]; - return $.param(query_elements); - }, - filter_grants: function(filters, event) { - if (event) { - event.preventDefault(); - } - if (filters.type == 'all' && location.href.indexOf('grants/explorer') == -1) { - location.href = '/grants/explorer'; - return false; - } - let current_style; + // loop through all URL params + for (let p of params) { + const param_key = p[0]; + const param_value = p[1]; - if (filters.type !== null && filters.type !== undefined) { - if (!current_style) { - current_style = document.all_type_styles[filters.type]; - } - this.current_type = filters.type; - if (this.current_type === 'collections') { - this.collection_id = null; + if (typeof vm.params[param_key] === 'object') { + if ((param_value.length > 0)) { + vm.params[param_key] = param_value.split(','); + } else { + vm.params[param_key] = []; + } + } else if (param_is_array.includes(param_key)) { + vm.params[param_key] = param_value.split(','); + } else if ([ 'true', 'false' ].includes(param_value)) { + vm.params[param_key] = param_value == 'true'; + } else { + vm.params[param_key] = param_value; } } - if (filters.category !== null && filters.category !== undefined) { - this.category = filters.category; - } - if (filters.keyword !== null && filters.keyword !== undefined) { - this.keyword = filters.keyword; - } - if (filters.following !== null && filters.following !== undefined) { - this.following = filters.following; - } - if (filters.idle_grants !== null && filters.idle_grants !== undefined) { - this.idle_grants = filters.idle_grants; - } - if (filters.sort !== null && filters.sort !== undefined) { - this.sort = filters.sort; - } - if (filters.show_contributions !== null && filters.show_contributions !== undefined) { - this.show_contributions = filters.show_contributions; - } - if (filters.featured !== null && filters.featured !== undefined) { - this.featured = filters.featured; - } - if (filters.network !== null && filters.network !== undefined) { - this.network = filters.network; - } + }, + updateUrlParams: function(replaceHistory) { + let vm = this; - if (filters.type === 'collections') { - this.collectionsPage = 1; + vm.searchParams = new URLSearchParams(vm.params); + + if (replaceHistory) { + window.history.replaceState({}, '', `${location.pathname}?${vm.searchParams}`); } else { - this.clearSingleCollection(); + window.history.pushState({}, '', `${location.pathname}?${vm.searchParams}`); } - this.page = 1; - this.setCurrentType(this.current_type); - this.fetchGrants(this.page); - }, - changeBanner: function() { - this.regex_style = document.all_routing_policies && - document.all_routing_policies.find(policy => { - return new RegExp(policy.url_pattern).test(window.location.href); - }); - this.toggleStyle(this.regex_style || document.current_style); - }, - clearSingleCollection: function() { - this.grants = []; - this.collections = []; - this.collection_id = null; - this.activeCollection = null; - this.page = 1; - this.updateURI(); - this.fetchGrants(); - }, - showSingleCollection: function(collectionId) { - this.collection_id = collectionId; - this.collections = []; - this.keyword = ''; - this.grants = []; - this.page = 1; - this.current_type = 'collections'; - this.updateURI(); - this.fetchGrants(); - }, - fetchGrants: async function(page, append_mode) { + unshiftGrants: async function(page) { let vm = this; - if (this.lock) - return; + await vm.updateUrlParams(); - this.lock = true; + vm.searchParams.set('page', page); + vm.fetchedPages.push(page); - const base_params = { - page: page || this.page, - limit: this.limit, - sort_option: this.sort, - network: this.network, - keyword: this.keyword, - state: this.state, - collections_page: this.collectionsPage, - category: this.category, - type: this.current_type - }; - - if (this.following) { - base_params['following'] = this.following; - } + const getGrants = await fetchData(`/grants/cards_info?${vm.searchParams.toString()}`); - if (this.idle_grants) { - base_params['idle'] = this.idle_grants; - } + getGrants.grants.forEach(function(item) { + vm.grants.unshift(item); - if (this.show_contributions) { - base_params['only_contributions'] = this.show_contributions; - } + }); - if (this.featured) { - base_params['featured'] = this.featured; - } - if (this.current_type === 'collections' && this.collection_id) { - base_params['collection_id'] = this.collection_id; - } + }, + fetchGrants: async function(page, append_mode, replaceHistory) { + let vm = this; - if (vm.round_num) { - base_params['round_num'] = vm.round_num; + console.log(page); + if (page) { + vm.params.page = page; } - if (vm.sub_round_slug) { - base_params['sub_round_slug'] = vm.sub_round_slug; - } + // let urlParams = new URLSearchParams(window.location.search); + // let searchParams = new URLSearchParams(vm.params); - if (vm.customer_name) { - base_params['customer_name'] = vm.customer_name; - } + await vm.updateUrlParams(replaceHistory); + + if (this.lock) + return; - const params = new URLSearchParams(base_params).toString(); - const getGrants = await fetchData(`/grants/cards_info?${params}`); + this.lock = true; + const getGrants = await fetchData(`/grants/cards_info?${vm.searchParams.toString()}`); if (!append_mode) { vm.grants = []; @@ -410,17 +372,22 @@ if (document.getElementById('grants-showcase')) { vm.grants.push(item); }); - if (this.collection_id) { - if (getGrants.collections.length > 0) { - this.activeCollection = getGrants.collections[0]; - } - } else if (this.current_type === 'collections') { - getGrants.collections.forEach(function(item) { - vm.collections.push(item); - }); - } else { - vm.collections = getGrants.collections; - } + // if (page) { + // vm.fetchedPages = [page]; + // } else { + // } + vm.fetchedPages = [ ...vm.fetchedPages, Number(vm.params.page) ]; + // if (this.params.collection_id) { + // if (getGrants.collections.length > 0) { + // this.activeCollection = getGrants.collections[0]; + // } + // } else if (this.current_type === 'collections') { + // getGrants.collections.forEach(function(item) { + // vm.collections.push(item); + // }); + // } else { + // vm.collections = getGrants.collections; + // } vm.credentials = getGrants.credentials; vm.grant_types = getGrants.grant_types; @@ -432,73 +399,107 @@ if (document.getElementById('grants-showcase')) { vm.changeBanner(); if (vm.grantsHasNext) { - vm.page = ++vm.page; - } else { - vm.page = 1; + vm.params.page = ++vm.params.page; } + this.updateCartData({ + detail: { + list: CartData.loadCart() + } + }); + vm.lock = false; return vm.grants; }, - scrollEnd: async function(event) { + tabChange: function(input) { let vm = this; - const scrollY = window.scrollY; - const visible = document.documentElement.clientHeight; - const pageHeight = document.documentElement.scrollHeight - 500; - const bottomOfPage = visible + scrollY >= pageHeight; + vm.tabSelected = vm.$refs.grantstabs.tabs[input].id; + console.log(vm.tabSelected); + vm.changeQuery({tab: vm.tabSelected}); + vm.unobserveFilter(); + vm.params.profile = false; + // vm.updateUrlParams(); - if (bottomOfPage || pageHeight < visible) { - if (vm.grantsHasNext) { - vm.fetchGrants(vm.page, true); - vm.grantsHasNext = false; - } + if (vm.tabSelected === 'collections') { + this.fetchCollections(); + } else { + this.fetchGrants(1); + setTimeout(() => vm.observeFilter()); + } + }, + loadTab: function() { + let vm = this; + let loadParams = new URLSearchParams(document.location.search); + const tabStrings = [ + {'index': 0, 'string': 'grants'}, + {'index': 1, 'string': 'collections'} + ]; + + if (loadParams.has('tab')) { + vm.tabSelected = loadParams.get('tab'); + console.log(tabStrings.filter(tab => tab.string === vm.tabSelected)[0].index); + vm.tabIndex = tabStrings.filter(tab => tab.string === vm.tabSelected)[0].index; + console.log(vm.tabIndex); + } + + if (vm.tabSelected === 'collections') { + // vm.updateUrlParams(); + this.fetchCollections(); + } else { + this.fetchGrants(undefined, undefined, true); } }, - addAllToCart: async function() { - if (this.cart_lock) + fetchCollections: async function(append_mode) { + let vm = this; + + if (vm.loadingCollections) return; - this.cart_lock = true; + vm.loadingCollections = true; + await vm.updateUrlParams(); - const base_params = { - no_pagination: true, - sort_option: this.sort, - network: this.network, - keyword: this.keyword, - state: this.state, - category: this.category, - type: this.current_type - }; + // vm.updateUrlParams(); - if (this.clr_round_pk) { - base_params['clr_round'] = this.clr_round_pk; - } + let url = `/api/v0.1/grants_collections/?${(vm.params.profile ? 'profile=' + vm.params.profile : '')}`; - if (this.following) { - base_params['following'] = this.following; + if (vm.collectionsPage) { + url = vm.collectionsPage; } + let getCollections = await fetch(url); + let collectionsJson = await getCollections.json(); - if (this.idle_grants) { - base_params['idle'] = this.idle_grants; - } + console.log(collectionsJson); - if (this.show_contributions) { - base_params['only_contributions'] = this.show_contributions; + if (append_mode) { + vm.collections = [ ...vm.collections, ...collectionsJson.results ]; + } else { + vm.collections = collectionsJson.results; } - const params = new URLSearchParams(base_params).toString(); - const getGrants = await fetchData(`/grants/bulk_cart?${params}`); - + vm.collectionsPage = collectionsJson.next; + vm.loadingCollections = false; + }, + scrollEnd: async function(event) { + let vm = this; - (getGrants.grants || []).forEach((grant) => { - CartData.addToCart(grant, true); - }); + const scrollY = window.scrollY; + const visible = document.documentElement.clientHeight; + const pageHeight = document.documentElement.scrollHeight - 500; + const bottomOfPage = visible + scrollY >= pageHeight; + const topOfPage = visible + scrollY <= pageHeight; + // console.log(bottomOfPage, pageHeight, visible, topOfPage); - showSideCart(); - _alert(`Congratulations, ${getGrants.grants.length} ${getGrants.grants.length > 1 ? 'grants were' : 'grants was'} added to your cart!`, 'success'); - this.cart_lock = false; + if (bottomOfPage || pageHeight < visible) { + console.log('bottmpage'); + if (vm.params.tab === 'collections' && vm.collectionsPage) { + vm.fetchCollections(true); + } else if (vm.grantsHasNext && !vm.pageIsFetched(vm.params.page + 1)) { + vm.fetchGrants(vm.params.page, true, true); + vm.grantsHasNext = false; + } + } }, removeCollection: async function({collection, grant, event}) { const getGrants = await fetchData(`/grants/v1/api/collections/${collection.id}/grants/remove`, 'POST', { @@ -506,9 +507,109 @@ if (document.getElementById('grants-showcase')) { }); this.grants = getGrants.grants; + }, + tagSearch(search, loading) { + const vm = this; + + // if (search.length < 3) { + // return; + // } + loading(true); + vm.getTag(loading, search); + + }, + getTag: async function(loading, search) { + console.log(search); + const vm = this; + const myHeaders = new Headers(); + const url = `/api/v0.1/grants_tag/?name=${escape(search)}`; + + myHeaders.append('X-Requested-With', 'XMLHttpRequest'); + return new Promise(resolve => { + + fetch(url, { + credentials: 'include', + headers: myHeaders + }).then(res => { + res.json().then(json => { + vm.$set(vm, 'tagsOptions', json); + + resolve(); + }); + if (loading) { + loading(false); + } + }); + }); + }, + updateCartData: function(e) { + const grants_in_cart = (e && e.detail && e.detail.list && e.detail.list) || []; + const grant_ids_in_cart = grants_in_cart.map((grant) => grant.grant_id); + + this.cart_data_count = grants_in_cart.length; + this.grants.forEach((grant) => { + vm.$set(grant, 'isInCart', (grant_ids_in_cart.indexOf(String(grant.id)) !== -1)); + }); + }, + scrollBottom: function() { + this.bottom = this.scrollEnd(); + }, + closeDropdown(ref) { + // Close the menu and (by passing true) return focus to the toggle button + this.$refs[ref].hide(true); + }, + observeFilter() { + // ensure the ref is in the dom before observing + if (this.$refs.filterNav) { + this.observed = this.$refs.filterNav; + // check for sticky position + this.observer = new IntersectionObserver( + ([e]) => { + this.sticky_active = e.intersectionRatio < 1; + }, + { threshold: [1] } + ); + // attach the observer + this.observer.observe(this.observed); + } + }, + unobserveFilter() { + if (this.observed) { + this.observer.unobserve(this.observed); + } + }, + watchHistory(event) { + this.getUrlParams(); + this.fetchGrants(1, undefined, true); + }, + pageIsFetched(page) { + let vm = this; + + return vm.fetchedPages.includes(page); + } }, computed: { + lowestPage() { + let vm = this; + + return Math.min(...vm.fetchedPages); + }, + grantsHasPrev() { + let vm = this; + + return vm.lowestPage > 1; + }, + currentCLR() { + let vm = this; + + if (!vm.clrData.results) + return; + + return vm.clrData?.results.find(item => { + return item.sub_round_slug == vm.params?.sub_round_slug; + }); + }, isGrantExplorer() { return (this.activePage == 'grants_explorer'); }, @@ -522,89 +623,37 @@ if (document.getElementById('grants-showcase')) { return true; } } - }, beforeMount() { - window.addEventListener('scroll', () => { - this.bottom = this.scrollEnd(); - }, false); + this.getUrlParams(); + this.loadTab(); + window.addEventListener('scroll', this.scrollBottom, {passive: true}); + // watch for history changes + window.addEventListener('popstate', this.watchHistory); }, beforeDestroy() { - window.removeEventListener('scroll', () => { - this.bottom = this.scrollEnd(); - }); + window.removeEventListener('scroll', this.scrollBottom); + window.removeEventListener('cartDataUpdated', this.updateCartData); + window.removeEventListener('popstate', this.watchHistory); + this.observer.unobserve(vm.$refs.filterNav); }, mounted() { let vm = this; - this.fetchGrants(this.page); - + vm.fetchClrGrants(); + // vm.fetchGrants(vm.params.page); + vm.getTag(undefined, ''); + // delay work till next tick to make sure els are present + vm.$nextTick(()=>{ + // check for sticky position + vm.observeFilter(); + // watch for cartUpdates + window.addEventListener('cartDataUpdated', this.updateCartData); + // hide cart dropdown on show of any other + vm.$root.$on('bv::dropdown::show', bvEvent => { + $('.navCart.dropdown').dropdown('hide'); + }); + }); } }); } - -const etherscanUrlConvert = (elem, network) => { - elem.each(function() { - $(this).attr('href', get_etherscan_url($(this).attr('href'), network)); - }); -}; - - -var glow_skip = function() { - setTimeout(function() { - $('#skip').animate({color: '#999'}, {duration: 1000}); - setTimeout(function() { - $('#skip').animate({color: '#bbb'}, {duration: 2000}); - }, 1500); - }, 1000); -}; - -setInterval(glow_skip, 5000); -glow_skip(); - -$(document).ready(function() { - $('.selected').parents('.accordion').trigger('click'); -}); - -$('#expand').on('click', () => { - $('#expand').hide(); - $('#minimize').show(); - $('#sidebar_container form#filters').css({ - 'height': 'auto', - 'display': 'inherit' - }); -}); - -$('#minimize').on('click', () => { - $('#minimize').hide(); - $('#expand').show(); - $('#sidebar_container form#filters').css({ - 'height': 0, - 'display': 'none' - }); -}); - -$(document).on('click', '.following-action', async(e) => { - e.preventDefault(); - const element = (e.target.tagName === 'BUTTON') ? $(e.target) : $(e.target.parentElement); - const grantId = element.data('grant'); - const favorite_url = `/grants/${grantId}/favorite`; - - let response = await fetchData(favorite_url, 'POST'); - - if (response.action === 'follow') { - element.find('i').addClass('fa'); - element.find('i').removeClass('far'); - element.find('span').text('Following'); - element.removeClass('text-muted'); - } else { - element.find('i').removeClass('fa'); - element.find('i').addClass('far'); - element.find('span').text('Follow'); - element.addClass('text-muted'); - - if (window.location.pathname === '/grants/following') { - element.closest('.grant-card').hide(); - } - } -}); diff --git a/app/assets/v2/js/grants/landingpage.js b/app/assets/v2/js/grants/landingpage.js index e9e8be0774f..d31d889eb04 100644 --- a/app/assets/v2/js/grants/landingpage.js +++ b/app/assets/v2/js/grants/landingpage.js @@ -2,79 +2,7 @@ const grantsNumPages = ''; const grantsHasNext = false; const numGrants = ''; -$(document).ready(() => { - $('#sort_option').select2({ - minimumResultsForSearch: Infinity - }); - - if (document.location.href.indexOf('/cart') == -1) { - localStorage.setItem('last_all_grants_index', document.location.href); - localStorage.setItem('last_all_grants_title', $('title').text().split('|')[0]); - } - - $('.select2-selection__rendered').removeAttr('title'); -}); - -// Vue.component('grant-sidebar', { -// props: [ -// 'filter_grants', 'grant_types', 'type', 'selected_category', 'keyword', 'following', 'set_type', -// 'idle_grants', 'show_contributions', 'query_params', 'round_num', 'sub_round_slug', 'customer_name', -// 'featured' -// ], -// data: function() { -// return { -// search: this.keyword, -// show_filters: false, -// handle: document.contxt.github_handle -// }; -// }, -// methods: { -// toggleFollowing: function(state, event) { -// event.preventDefault; -// this.filter_grants({following: state}); -// }, -// toggleIdle: function(state, event) { -// event.preventDefault; -// this.filter_grants({idle_grants: state}); -// }, -// toggleContributionView: function(state, event) { -// event.preventDefault; -// this.filter_grants({show_contributions: state}); -// }, -// toggleMyGrants: function(state, event) { -// let me = state ? 'me' : 'all'; - -// event.preventDefault; -// this.filter_grants({type: me, category: ''}); -// }, -// isMobileDevice: function() { -// return window.innerWidth < 576; -// }, -// filterLink: function(params) { -// return this.filter_grants(params); -// }, -// searchKeyword: function() { -// if (this.timeout) { -// clearTimeout(this.timeout); -// } - -// this.timeout = setTimeout(() => { -// this.filter_grants({keyword: this.search}); -// }, 1000); -// }, -// onResize: function() { -// if (!this.isMobileDevice() && this.show_filters !== null) { -// this.show_filters = null; -// } else if (this.isMobileDevice() && this.show_filters === null) { -// this.show_filters = false; -// } -// } -// }, -// mounted() { -// window.addEventListener('resize', this.onResize); -// } -// }); if (document.getElementById('grants-showcase')) { let sort = getParam('sort'); @@ -90,7 +18,7 @@ if (document.getElementById('grants-showcase')) { grants: [], grant: {}, page: 1, - collectionsPage: 1, + collectionsPage: null, limit: 6, show_active_clrs: window.localStorage.getItem('show_active_clrs') != 'false', sort: sort, @@ -122,271 +50,44 @@ if (document.getElementById('grants-showcase')) { numGrants, mainBanner: document.current_style, visibleModal: false, - bannerCollapsed: false + bannerCollapsed: false, + loadingCollections: false }, methods: { - toggleActiveCLRs() { - this.show_active_clrs = !this.show_active_clrs; - window.localStorage.setItem('show_active_clrs', this.show_active_clrs); - }, - setView: function(mode, event) { - event.preventDefault(); - localStorage.setItem('grants_view', mode); - this.view = mode; - }, - setCurrentType: function(currentType) { - this.current_type = currentType; - - if (this.current_type === 'collections') { - this.clearSingleCollection(); - } - - this.updateURI(); - }, - updateURI: function() { - const vm = this; - const q = vm.getQueryParams(); - - if (vm.round_num) { - let uri = `/grants/clr/${vm.round_num}/`; - - if (vm.sub_round_slug && !vm.customer_name) { - uri = `/grants/clr/${vm.round_num}/${vm.sub_round_slug}/`; - } - - if (!vm.sub_round_slug && vm.customer_name) { - uri = `/grants/clr/${vm.customer_name}/${vm.round_num}/`; - } - - if (vm.sub_round_slug && vm.customer_name) { - uri = `/grants/clr/${vm.customer_name}/${vm.round_num}/${vm.sub_round_slug}/`; - } - - if (this.current_type === 'collections') { - window.history.pushState('', '', `${uri}?${q || ''}`); - } else { - window.history.pushState('', '', `${uri}?type=${this.current_type}&${q || ''}`); - } - } else { - const uri = '/grants/explorer/'; - - if (this.current_type === 'collections') { - window.history.pushState('', '', `${uri}?${q || ''}`); - } else { - window.history.pushState('', '', `${uri}${this.current_type}?${q || ''}`); - } - } - - if (this.current_type === 'activity') { - const triggerTS = function() { - const activeElement = $('.infinite-more-link'); - - if (activeElement.length) { - $('.infinite-more-link').click(); - } else { - setTimeout(triggerTS, 1000); - } - }; - - setTimeout(triggerTS, 1000); - } - }, - getQueryParams: function() { - const query_elements = {}; - - if (this.category && this.current_type !== 'all') { - query_elements['category'] = this.category; - } - - if (this.keyword) { - query_elements['keyword'] = this.keyword; - } - if (this.idle_grants) { - query_elements['idle'] = this.idle_grants; - } - if (this.following) { - query_elements['following'] = this.following; - } - if (this.show_contributions) { - query_elements['only_contributions'] = this.show_contributions; - } - if (this.featured) { - query_elements['featured'] = this.featured; - } - if (this.sort !== 'weighted_shuffle') { - query_elements['sort'] = this.sort; - } - if (this.network !== 'mainnet') { - query_elements['network'] = this.network; - } - if (this.current_type === 'collections') { - if (this.collection_id) { - query_elements['collection_id'] = this.collection_id; - } - } - - return $.param(query_elements); - }, - filter_grants: function(filters, event) { - if (event) { - event.preventDefault(); - } - let current_style; - - if (filters.type !== null && filters.type !== undefined) { - if (!current_style) { - current_style = document.all_type_styles[filters.type]; - } - this.current_type = filters.type; - if (this.current_type === 'collections') { - this.collection_id = null; - } - } - if (filters.category !== null && filters.category !== undefined) { - this.category = filters.category; - } - if (filters.keyword !== null && filters.keyword !== undefined) { - this.keyword = filters.keyword; - } - if (filters.following !== null && filters.following !== undefined) { - this.following = filters.following; - } - if (filters.idle_grants !== null && filters.idle_grants !== undefined) { - this.idle_grants = filters.idle_grants; - } - if (filters.sort !== null && filters.sort !== undefined) { - this.sort = filters.sort; - } - if (filters.show_contributions !== null && filters.show_contributions !== undefined) { - this.show_contributions = filters.show_contributions; - } - if (filters.featured !== null && filters.featured !== undefined) { - this.featured = filters.featured; - } - if (filters.network !== null && filters.network !== undefined) { - this.network = filters.network; - } - - if (filters.type === 'collections') { - this.collectionsPage = 1; - } - this.page = 1; - this.setCurrentType(this.current_type); - this.fetchGrants(this.page); - - }, - clearSingleCollection: function() { - this.grants = []; - this.collections = []; - this.collection_id = null; - this.activeCollection = null; - this.page = 1; - this.updateURI(); - this.fetchGrants(); - }, - showSingleCollection: function(collectionId) { - this.collection_id = collectionId; - this.collections = []; - this.keyword = ''; - this.grants = []; - this.page = 1; - this.current_type = 'collections'; - this.updateURI(); - this.fetchGrants(); - }, - fetchGrants: async function(page, append_mode) { - const vm = this; + fetchCollections: async function(append_mode) { + let vm = this; - if (this.lock) + if (vm.loadingCollections) return; - this.lock = true; - - const base_params = { - page: page || this.page, - limit: this.limit, - sort_option: this.sort, - network: this.network, - keyword: this.keyword, - state: this.state, - collections_page: this.collectionsPage, - category: this.category, - type: this.current_type - }; - - if (this.following) { - base_params['following'] = this.following; - } - - if (this.idle_grants) { - base_params['idle'] = this.idle_grants; - } + vm.loadingCollections = true; - if (this.show_contributions) { - base_params['only_contributions'] = this.show_contributions; - } + // vm.updateUrlParams(); - if (this.featured) { - base_params['featured'] = this.featured; - } + let url = '/api/v0.1/grants_collections/?featured=true'; - if (this.current_type === 'collections' && this.collection_id) { - base_params['collection_id'] = this.collection_id; + if (vm.collectionsPage) { + url = vm.collectionsPage; } + let getCollections = await fetch(url); + let collectionsJson = await getCollections.json(); - if (vm.round_num) { - base_params['round_num'] = vm.round_num; - } - - if (vm.sub_round_slug) { - base_params['sub_round_slug'] = vm.sub_round_slug; - } - - if (vm.customer_name) { - base_params['customer_name'] = vm.customer_name; - } - - const params = new URLSearchParams(base_params).toString(); - const getGrants = await fetchData(`/grants/cards_info?${params}`); - - if (!append_mode) { - vm.grants = []; - } - getGrants.grants.forEach(function(item) { - vm.grants.push(item); - }); + console.log(collectionsJson); - if (this.collection_id) { - if (getGrants.collections.length > 0) { - this.activeCollection = getGrants.collections[0]; - } + if (append_mode) { + vm.collections = [ ...vm.collections, ...collectionsJson.results ]; } else { - if (this.current_type === 'collections') { - getGrants.collections.forEach(function(item) { - vm.collections.push(item); - }); - } else { - vm.collections = getGrants.collections; - } - - vm.credentials = getGrants.credentials; - vm.grant_types = getGrants.grant_types; - vm.contributions = getGrants.contributions; + vm.collections = collectionsJson.results; } - vm.grantsNumPages = getGrants.num_pages; - vm.grantsHasNext = getGrants.has_next; - vm.numGrants = getGrants.count; - - if (vm.grantsHasNext) { - vm.page = ++vm.page; - } else { - vm.page = 1; - } - vm.lock = false; + vm.collectionsPage = collectionsJson.next; + vm.loadingCollections = false; - return vm.grants; + }, + toggleActiveCLRs() { + this.show_active_clrs = !this.show_active_clrs; + window.localStorage.setItem('show_active_clrs', this.show_active_clrs); }, scrollEnd: async function(event) { const vm = this; @@ -396,59 +97,9 @@ if (document.getElementById('grants-showcase')) { const pageHeight = document.documentElement.scrollHeight - 500; const bottomOfPage = visible + scrollY >= pageHeight; - if (bottomOfPage || pageHeight < visible) { - if (vm.grantsHasNext) { - vm.fetchGrants(vm.page, true); - vm.grantsHasNext = false; - } - } - }, - addAllToCart: async function() { - if (this.cart_lock) - return; - - this.cart_lock = true; - - const base_params = { - no_pagination: true, - sort_option: this.sort, - network: this.network, - keyword: this.keyword, - state: this.state, - category: this.category, - type: this.current_type - }; - - if (this.following) { - base_params['following'] = this.following; - } - - if (this.idle_grants) { - base_params['idle'] = this.idle_grants; - } - - if (this.show_contributions) { - base_params['only_contributions'] = this.show_contributions; - } - - const params = new URLSearchParams(base_params).toString(); - const getGrants = await fetchData(`/grants/bulk_cart?${params}`); - - - (getGrants.grants || []).forEach((grant) => { - CartData.addToCart(grant, true); - }); + // if (bottomOfPage || pageHeight < visible) { - showSideCart(); - _alert(`Congratulations, ${getGrants.grants.length} ${getGrants.grants.length > 1 ? 'grants were' : 'grants was'} added to your cart!`, 'success'); - this.cart_lock = false; - }, - removeCollection: async function({collection, grant, event}) { - const getGrants = await fetchData(`v1/api/collections/${collection.id}/grants/remove`, 'POST', { - 'grant': grant.id - }); - - this.grants = getGrants.grants; + // } }, showModal(modalName) { this.visibleModal = modalName; @@ -468,6 +119,7 @@ if (document.getElementById('grants-showcase')) { } }, beforeMount() { + this.fetchCollections(); window.addEventListener('scroll', () => { this.bottom = this.scrollEnd(); }, false); @@ -480,86 +132,8 @@ if (document.getElementById('grants-showcase')) { mounted() { const vm = this; - vm.current_type = 'collections'; vm.bannerCollapsed = localStorage.getItem('bannerCollapsed') == 'true'; - - vm.fetchGrants(vm.page); - - $('#sort_option2').select2({ - minimumResultsForSearch: Infinity, - templateSelection: function(data, container) { - // Add custom attributes to the