Skip to content

Commit

Permalink
feat: support adding grants to cart by saving them to localStorage
Browse files Browse the repository at this point in the history
  • Loading branch information
mds1 committed Aug 18, 2021
1 parent fb302c6 commit 4f463b2
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 46 deletions.
91 changes: 71 additions & 20 deletions app/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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);
}
7 changes: 4 additions & 3 deletions app/src/views/GrantRegistryGrantDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
Metadata URL: <a class="link" :href="grant.metaPtr" target="_blank">{{ grant.metaPtr }}</a>
</p>

<button v-if="isOwner" @click="isEditing = true" class="mt-5 btn btn-primary">Edit Grant</button>
<button @click="addToCart(grant)" class="mt-5 btn btn-primary">Add to Cart</button>
<button v-if="isOwner" @click="isEditing = true" class="mt-5 btn btn-secondary">Edit Grant</button>
</div>

<!-- Editing grant -->
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -169,7 +170,7 @@ export default defineComponent({
name: 'GrantRegistryGrantDetail',
components: { BaseInput },
setup() {
return { ...useGrantDetail() };
return { ...useGrantDetail(), addToCart };
},
});
</script>
52 changes: 29 additions & 23 deletions app/src/views/GrantRegistryList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,63 +15,69 @@
<li
v-for="grant in grants"
:key="grant.id.toString()"
@click="pushRoute({ name: 'dgrants-id', params: { id: grant.id.toString() } })"
class="
col-span-1
bg-white
rounded-lg
shadow
divide-y divide-gray-400 divide-opacity-30
cursor-pointer
border border-gray-200
hover:border-primary-500
"
>
<div class="w-full flex items-center justify-between p-6 space-x-6 hover:border">
<div class="flex-1 truncate text-left">
<div class="flex items-center space-x-3">
<h3 class="text-gray-900 text-sm font-medium truncate">Grant ID: {{ grant.id.toString() }}</h3>
</div>
<p class="mt-1 text-gray-500 text-sm truncate">{{ grant.metaPtr }}</p>
</div>
</div>
<div>
<div class="pl-6 p-2 -mt-px flex divide-x divide-gray-400 divide-opacity-30">
<div class="w-0 flex-1 flex">
<div class="flex-1 truncate text-left">
<p class="mt-1 text-gray-500 text-sm truncate">Owner</p>
<div class="flex items-center space-x-3">
<h3 class="text-gray-900 text-sm font-medium">{{ formatAddress(grant.owner) }}</h3>
</div>
<div
@click="pushRoute({ name: 'dgrants-id', params: { id: grant.id.toString() } })"
class="cursor-pointer divide-y divide-gray-400 divide-opacity-30"
>
<div class="w-full flex items-center justify-between p-6 space-x-6 hover:border">
<div class="flex-1 truncate text-left">
<div class="flex items-center space-x-3">
<h3 class="text-gray-900 text-sm font-medium truncate">Grant ID: {{ grant.id.toString() }}</h3>
</div>
<p class="mt-1 text-gray-500 text-sm truncate">{{ grant.metaPtr }}</p>
</div>
<div class="pl-6 -ml-px w-0 flex-1 flex">
</div>
<div>
<div class="pl-6 p-2 -mt-px flex divide-x divide-gray-400 divide-opacity-30">
<div class="w-0 flex-1 flex">
<div class="flex-1 truncate text-left">
<p class="mt-1 text-gray-500 text-sm truncate">Payee</p>
<p class="mt-1 text-gray-500 text-sm truncate">Owner</p>
<div class="flex items-center space-x-3">
<h3 class="text-gray-900 text-sm font-medium">{{ formatAddress(grant.payee) }}</h3>
<h3 class="text-gray-900 text-sm font-medium">{{ formatAddress(grant.owner) }}</h3>
</div>
</div>
</div>
<div class="pl-6 -ml-px w-0 flex-1 flex">
<div class="w-0 flex-1 flex">
<div class="flex-1 truncate text-left">
<p class="mt-1 text-gray-500 text-sm truncate">Payee</p>
<div class="flex items-center space-x-3">
<h3 class="text-gray-900 text-sm font-medium">{{ formatAddress(grant.payee) }}</h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="divide-x divide-gray-400 divide-opacity-30">
<button @click="addToCart(grant)" class="my-2 btn btn-primary">Add to Cart</button>
</div>
</li>
</ul>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { formatAddress, pushRoute } from 'src/utils/utils';
import { addToCart, formatAddress, pushRoute } from 'src/utils/utils';
import useDataStore from 'src/store/data';
export default defineComponent({
name: 'GrantRegistryList',
setup() {
const { grants } = useDataStore();
return { formatAddress, pushRoute, grants };
return { addToCart, formatAddress, pushRoute, grants };
},
});
</script>

0 comments on commit 4f463b2

Please sign in to comment.