From 4f463b2780bc282573b0209d0499b5c6e2ad8467 Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Thu, 12 Aug 2021 14:03:56 -0700 Subject: [PATCH] feat: support adding grants to cart by saving them to localStorage --- app/src/utils/utils.ts | 91 +++++++++++++++++----- app/src/views/GrantRegistryGrantDetail.vue | 7 +- app/src/views/GrantRegistryList.vue | 52 +++++++------ 3 files changed, 104 insertions(+), 46 deletions(-) diff --git a/app/src/utils/utils.ts b/app/src/utils/utils.ts index aab32e0e..2e9ea4d8 100644 --- a/app/src/utils/utils.ts +++ b/app/src/utils/utils.ts @@ -3,21 +3,34 @@ */ import router from 'src/router/index'; import { RouteLocationRaw } from 'vue-router'; -import { BigNumber, isAddress } from 'src/utils/ethers'; +import { BigNumber, isAddress, parseUnits } from 'src/utils/ethers'; import { BigNumberish, Contract, ContractTransaction } from 'ethers'; -import { GrantRound } from '@dgrants/types'; +import { Grant, GrantRound } from '@dgrants/types'; +// --- Formatters --- // Returns an address with the following format: 0x1234...abcd export function formatAddress(address: string) { if (!address || address.length !== 42) return null; return `${address.slice(0, 6)}...${address.slice(38)}`; } -// Navigates to the specified page and pushes a new entry into the history stack -export async function pushRoute(to: RouteLocationRaw) { - await router.push(to); +// Expects a unix timestamp and will return a human readable message of how far in the past/future it is +export function daysAgo(val = 0) { + // Use a formatter to establish "in 10 days" vs "10 days ago" + const formatter = new Intl.RelativeTimeFormat(); + // Number of days since now + const deltaDays = (val * 1000 - Date.now()) / (1000 * 3600 * 24); + + // Format "days ago" as string + return formatter.format(Math.round(deltaDays), 'days'); } +// convert a unix ts to a toLocaleString +export function unixToLocaleString(time: BigNumberish) { + return new Date(BigNumber.from(time).toNumber() * 1000).toLocaleString(); +} + +// --- Validation --- // Returns true if the provided URL is a valid URL export function isValidUrl(val: string | undefined) { return val && val.includes('://'); // TODO more robust URL validation @@ -28,22 +41,60 @@ export function isValidAddress(val: string | undefined) { return val && isAddress(val); } -// Expects a unix timestamp and will return a human readable message of how far in the past/future it is -export function daysAgo(val = 0) { - // Use a formatter to establish "in 10 days" vs "10 days ago" - const formatter = new Intl.RelativeTimeFormat(); - // Number of days since now - const deltaDays = (val * 1000 - Date.now()) / (1000 * 3600 * 24); +// --- Grants + Cart --- +const CART_KEY = 'cart'; +const DEFAULT_CONTRIBUTION_TOKEN_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; // DAI +const DEFAULT_CONTRIBUTION_AMOUNT = parseUnits('5', 18).toString(); // DAI has 18 decimals +type CartItem = { + grantId: string; + contributionTokenAddress: string; // store address instead of TokenInfo to reduce localStorage size used + contributionAmount: string; +}; - // Format "days ago" as string - return formatter.format(Math.round(deltaDays), 'days'); +// Loads cart data +export function loadCart(): CartItem[] { + // Return empty array if nothing found + const rawCart = localStorage.getItem(CART_KEY); + if (!rawCart) return []; + + // Parse the data. If the data is an array, return it, + const cart = JSON.parse(rawCart); + if (Array.isArray(cart)) return cart; + + // Otherwise clear the localStorage key and return empty array + localStorage.removeItem(CART_KEY); + return []; } -// convert a unix ts to a toLocaleString -export function unixToLocaleString(time: BigNumberish) { - return new Date(BigNumber.from(time).toNumber() * 1000).toLocaleString(); +// Adds a grant to the cart +export function addToCart(grant: Grant) { + // If this grant is already in the cart, do nothing + const cart = loadCart(); + if (cart.map((grant) => grant.grantId).includes(grant.id.toString())) return; + + // Otherwise, add it to the cart and update localStorage + cart.push({ + grantId: grant.id.toString(), + contributionTokenAddress: DEFAULT_CONTRIBUTION_TOKEN_ADDRESS, + contributionAmount: DEFAULT_CONTRIBUTION_AMOUNT, + }); + localStorage.setItem(CART_KEY, JSON.stringify(cart)); +} + +// Removes a grant from the cart +export function removeFromCart(grantId: BigNumberish) { + const cart = loadCart(); + const newCart = cart.filter((grant) => grant.grantId !== BigNumber.from(grantId).toString()); + localStorage.setItem(CART_KEY, JSON.stringify(newCart)); } +// Check against the grantRounds status for a match +export function hasStatus(status: string) { + // returns a fn (currying the given status) + return (round: GrantRound) => round.status === status; +} + +// --- Tokens --- // Check for approved allowance export async function checkAllowance(token: Contract, ownerAddress: string | undefined, spenderAddress: string) { // return the balance held for userAddress @@ -58,8 +109,8 @@ export async function getApproval(token: Contract, address: string, amount: BigN await tx.wait(); } -// Check against the grantRounds status for a match -export function hasStatus(status: string) { - // returns a fn (currying the given status) - return (round: GrantRound) => round.status === status; +// --- Other --- +// Navigates to the specified page and pushes a new entry into the history stack +export async function pushRoute(to: RouteLocationRaw) { + await router.push(to); } diff --git a/app/src/views/GrantRegistryGrantDetail.vue b/app/src/views/GrantRegistryGrantDetail.vue index d651fd83..cd93b4a8 100644 --- a/app/src/views/GrantRegistryGrantDetail.vue +++ b/app/src/views/GrantRegistryGrantDetail.vue @@ -10,7 +10,8 @@ Metadata URL: {{ grant.metaPtr }}

- + + @@ -88,7 +89,7 @@ import useWalletStore from 'src/store/wallet'; // --- Methods and Data --- import { GRANT_REGISTRY_ADDRESS, GRANT_REGISTRY_ABI } from 'src/utils/constants'; import { Contract, ContractTransaction } from 'src/utils/ethers'; -import { isValidAddress, isValidUrl } from 'src/utils/utils'; +import { addToCart, isValidAddress, isValidUrl } from 'src/utils/utils'; // --- Types --- import { GrantRegistry } from '@dgrants/contracts'; @@ -169,7 +170,7 @@ export default defineComponent({ name: 'GrantRegistryGrantDetail', components: { BaseInput }, setup() { - return { ...useGrantDetail() }; + return { ...useGrantDetail(), addToCart }; }, }); diff --git a/app/src/views/GrantRegistryList.vue b/app/src/views/GrantRegistryList.vue index 33b5e8e0..0a775839 100644 --- a/app/src/views/GrantRegistryList.vue +++ b/app/src/views/GrantRegistryList.vue @@ -15,48 +15,54 @@
  • -
    -
    -
    -

    Grant ID: {{ grant.id.toString() }}

    -
    -

    {{ grant.metaPtr }}

    -
    -
    -
    -
    -
    -
    -

    Owner

    -
    -

    {{ formatAddress(grant.owner) }}

    -
    +
    +
    +
    +
    +

    Grant ID: {{ grant.id.toString() }}

    +

    {{ grant.metaPtr }}

    -
    +
    +
    +
    -

    Payee

    +

    Owner

    -

    {{ formatAddress(grant.payee) }}

    +

    {{ formatAddress(grant.owner) }}

    +
    +
    +
    +
    +
    +
    +

    Payee

    +
    +

    {{ formatAddress(grant.payee) }}

    +
    +
    + +
  • @@ -64,14 +70,14 @@