embeddable-nfts
Advanced tools
Comparing version
{ | ||
"name": "embeddable-nfts", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Resuable, embeddable webcomponent for OpenSea assets.", | ||
@@ -21,8 +21,7 @@ "scripts": { | ||
}, | ||
"homepage": "https://github.com/ProjectOpenSea/embeddable_nfts#readme", | ||
"homepage": "https://github.com/ProjectOpenSea/embeddable-nfts#readme", | ||
"dependencies": { | ||
"lit-element": "^2.2.1", | ||
"lit-html": "^1.1.2", | ||
"my-package": "0.0.0", | ||
"opensea-js": "^0.7.1", | ||
"opensea-js": "^1.1.0", | ||
"web3": "0.20.6" | ||
@@ -36,12 +35,12 @@ }, | ||
"devDependencies": { | ||
"@babel/cli": "^7.8.3", | ||
"@babel/core": "^7.8.3", | ||
"@babel/cli": "^7.8.4", | ||
"@babel/core": "^7.8.7", | ||
"@babel/plugin-proposal-class-properties": "^7.8.3", | ||
"@babel/plugin-proposal-decorators": "^7.8.3", | ||
"@babel/preset-env": "^7.8.3", | ||
"@babel/preset-env": "^7.8.7", | ||
"@babel/preset-typescript": "^7.8.3", | ||
"@types/node": "^13.1.8", | ||
"@typescript-eslint/eslint-plugin": "^2.16.0", | ||
"@typescript-eslint/parser": "^2.16.0", | ||
"@webcomponents/webcomponentsjs": "^2.4.1", | ||
"@types/node": "^13.9.1", | ||
"@typescript-eslint/eslint-plugin": "^2.23.0", | ||
"@typescript-eslint/parser": "^2.23.0", | ||
"@webcomponents/webcomponentsjs": "^2.4.2", | ||
"html-webpack-plugin": "^3.2.0", | ||
@@ -52,10 +51,10 @@ "husky": "^4.2.3", | ||
"ts-loader": "^6.2.1", | ||
"tslint": "^5.20.1", | ||
"tslint": "^6.1.0", | ||
"tslint-eslint-rules": "^5.4.0", | ||
"typescript": "^3.7.5", | ||
"typescript": "^3.8.3", | ||
"web3-typescript-typings": "^0.10.2", | ||
"webpack": "^4.41.5", | ||
"webpack-cli": "^3.3.10", | ||
"webpack-dev-server": "^3.10.1" | ||
"webpack": "^4.42.0", | ||
"webpack-cli": "^3.3.11", | ||
"webpack-dev-server": "^3.10.3" | ||
} | ||
} |
@@ -15,3 +15,3 @@ # Embeddable NFTs | ||
`contractAddress`\*- The token's contract address. | ||
`tokenAddress`\*- The token's contract address. | ||
@@ -34,3 +34,3 @@ `tokenId`\* - The token Id of the asset. | ||
<nft-card | ||
contractAddress="0x5caebd3b32e210e85ce3e9d51638b9c445481567" | ||
tokenAddress="0x5caebd3b32e210e85ce3e9d51638b9c445481567" | ||
tokenId="2242579050293992223" | ||
@@ -37,0 +37,0 @@ network="mainnet" |
@@ -1,1 +0,13 @@ | ||
export const NO_WEB3_ERROR: string = 'You need an Ethereum wallet to interact with this marketplace. Unlock your wallet, get MetaMask.io or Portis on desktop, or get Trust Wallet or Coinbase Wallet on mobile.' | ||
import { ButtonType } from './types' | ||
export const NO_WEB3_ERROR: string = 'You need an Ethereum wallet to interact ' + | ||
'with this marketplace. Unlock your wallet, get MetaMask.io or ' + | ||
'Portis on desktop, or get Trust Wallet or Coinbase Wallet on mobile.' | ||
export const BTN_TEXT: { [index: string]: string } = { | ||
[ButtonType.Manage]: 'manage this item ❯', | ||
[ButtonType.Buy]: 'buy this item ❯', | ||
[ButtonType.View]: 'view on openSea ❯', | ||
[ButtonType.SwitchNetwork]: 'switch to ', | ||
[ButtonType.Unlock]: 'buy this item ❯' | ||
} |
import { LitElement, html, customElement, property, css } from 'lit-element' | ||
import { styleMap } from 'lit-html/directives/style-map' | ||
import { classMap } from 'lit-html/directives/class-map' | ||
import { OpenSeaTraitStats } from 'opensea-js/lib/types' | ||
import './info-button' | ||
import { Trait, TraitData, Traits, TraitType } from './types' | ||
enum TraitType { | ||
Property = 'prop', | ||
Stat = 'stat', | ||
Ranking = 'ranking', | ||
Boost = 'boost', | ||
} | ||
import { | ||
formatTraitType, | ||
getTraitType | ||
} from './utils' | ||
interface Traits { | ||
[index: string]: Trait[] | ||
props: Trait[] | ||
stats: Trait[] | ||
rankings: Trait[] | ||
boosts: Trait[] | ||
} | ||
interface Trait { | ||
value: string | number | ||
max?: string | number | ||
display_type?: string | ||
trait_type: string | ||
} | ||
interface TraitData { | ||
traits: Trait[] | ||
collectionTraits: CollectionTraits | ||
} | ||
interface CollectionTraits { | ||
[index: string]: OpenSeaTraitStats | ||
} | ||
const TRAIT_HEADER_HEIGHT = 42 | ||
@@ -82,2 +56,12 @@ const TRAIT_HEADER_MARGIN_BOTTOM = 8 | ||
@property({type: Object}) public traitData!: TraitData | ||
@property({type: Object}) public openseaLink?: string | ||
@property({type: Boolean}) public loading = true | ||
@property({type: Boolean}) public horizontal!: boolean | ||
@property({type: Number}) public cardHeight!: number | ||
@property({type: Number}) public cardInnerHeight?: number | ||
@property({type: Number}) public cardWidth!: number | ||
@property({type: Object}) private traits?: Traits | ||
static get styles() { | ||
@@ -254,52 +238,2 @@ return css` | ||
@property({type: Object}) public traitData!: TraitData | ||
@property({type: Object}) public openseaLink?: string | ||
@property({type: Boolean}) public loading = true | ||
@property({type: Boolean}) public horizontal!: boolean | ||
@property({type: Number}) public cardHeight!: number | ||
@property({type: Number}) public cardInnerHeight?: number | ||
@property({type: Number}) public cardWidth!: number | ||
@property({type: Object}) private traits?: Traits | ||
private static formatTraitType(traitType: string) { | ||
return traitType.replace(/_/g, ' ') | ||
} | ||
private static isBoost(trait: Trait) { | ||
return trait.display_type && trait.display_type.includes('boost') | ||
} | ||
private static isRanking(trait: Trait, collectionTraits: CollectionTraits) { | ||
return trait.display_type === null && trait.trait_type in collectionTraits && 'max' in collectionTraits[trait.trait_type] | ||
} | ||
/** | ||
* IsStat - Checks to see if the given trait is a 'Stat' | ||
* A 'Stat' is defined as any trait that has a `display_type` of 'number' | ||
* | ||
* @param trait - The object containing an asset's trait | ||
* @return true if the trait is a 'Stat' and false otherwise | ||
*/ | ||
private static isStat(trait: Trait) { | ||
return trait.display_type === 'number' | ||
} | ||
/** | ||
* IsProperty - Checks to see if the given trait is a 'Property'. | ||
* A 'Property' is defined as any trait that has a `display_type` of null | ||
* and does not have a min/max value | ||
* | ||
* @param trait - The object containing an asset's trait | ||
* @return true if the trait is a 'Property' and false otherwise | ||
*/ | ||
private static isProperty(trait: Trait, collectionTraits: CollectionTraits) { | ||
return ( | ||
trait.display_type === null && | ||
trait.trait_type in collectionTraits && | ||
!('max' in collectionTraits[trait.trait_type]) || | ||
!(trait.trait_type in collectionTraits) | ||
) | ||
} | ||
public updated(changedProperties: Map<string, string>) { | ||
@@ -392,3 +326,3 @@ // Assumption: If the traitData gets updated we should rebuild the | ||
<div class="trait_boost-name"> | ||
${NftCardBackTemplate.formatTraitType(trait_type)} | ||
${formatTraitType(trait_type)} | ||
</div> | ||
@@ -431,3 +365,3 @@ </div> | ||
<div class="stat-name"> | ||
${NftCardBackTemplate.formatTraitType(stat.trait_type)} | ||
${formatTraitType(stat.trait_type)} | ||
</div> | ||
@@ -469,3 +403,3 @@ </div> | ||
<div class="trait_ranking-header-name"> | ||
${NftCardBackTemplate.formatTraitType(trait_type)} | ||
${formatTraitType(trait_type)} | ||
</div> | ||
@@ -499,3 +433,3 @@ <div class="trait_ranking-header-value">${value} of ${max}</div> | ||
<div class="trait_property" style="${styleMap(propStyle)}"> | ||
<p class="trait_property-type">${NftCardBackTemplate.formatTraitType(trait_type)}</p> | ||
<p class="trait_property-type">${formatTraitType(trait_type)}</p> | ||
<p class="trait_property-value">${value}</p> | ||
@@ -572,3 +506,3 @@ </div> | ||
for (const trait of assetTraits) { | ||
const type = this.getTraitType(trait, collectionTraits) | ||
const type = getTraitType(trait, collectionTraits) | ||
@@ -584,18 +518,2 @@ const name = trait.trait_type | ||
} | ||
private getTraitType(trait: Trait, collectionTraits: CollectionTraits) { | ||
if (NftCardBackTemplate.isProperty(trait, collectionTraits)) { | ||
return TraitType.Property | ||
} | ||
if (NftCardBackTemplate.isRanking(trait, collectionTraits)) { | ||
return TraitType.Ranking | ||
} | ||
if (NftCardBackTemplate.isStat(trait)) { | ||
return TraitType.Stat | ||
} | ||
if (NftCardBackTemplate.isBoost(trait)) { | ||
return TraitType.Boost | ||
} | ||
return null // Default return statement | ||
} | ||
} |
@@ -1,9 +0,2 @@ | ||
import { | ||
LitElement, | ||
html, | ||
customElement, | ||
property, | ||
css, | ||
TemplateResult | ||
} from 'lit-element' | ||
import { css, customElement, html, LitElement, property } from 'lit-element' | ||
@@ -13,41 +6,16 @@ import { classMap } from 'lit-html/directives/class-map' | ||
import { OpenSeaAsset, OpenSeaFungibleToken, Order, Network } from 'opensea-js/lib/types' | ||
import { OpenSeaAsset, OpenSeaFungibleToken } from 'opensea-js/lib/types' | ||
/* lit-element classes */ | ||
import './info-button' | ||
import { toBaseDenomination } from './utils' | ||
import { ButtonType, PriceType, State } from './types' | ||
import { BTN_TEXT } from './constants' | ||
enum ButtonType { | ||
Manage = 'manage', | ||
Buy = 'buy', | ||
View = 'view', | ||
SwitchNetwork = 'switchNetwork', | ||
Unlock = 'unlock' | ||
} | ||
const BTN_TEXT: { [index: string]: string } = { | ||
[ButtonType.Manage]: 'manage this item ❯', | ||
[ButtonType.Buy]: 'buy this item ❯', | ||
[ButtonType.View]: 'view on openSea ❯', | ||
[ButtonType.SwitchNetwork]: 'switch to ', | ||
[ButtonType.Unlock]: 'buy this item ❯' | ||
} | ||
interface LastSaleData { | ||
paymentToken?: OpenSeaFungibleToken | ||
currentPrice: number | ||
expires: Date | ||
} | ||
interface State { | ||
isOwnedByAccount: boolean | ||
isMatchingNetwork: boolean | ||
isUnlocked: boolean | ||
hasWeb3: boolean | ||
network: Network | ||
} | ||
const TOKEN_DECIMALS = 18 | ||
@customElement('nft-card-front') | ||
export class NftCardFrontTemplate extends LitElement { | ||
@property({type: Object}) public asset?: OpenSeaAsset | ||
@property({type: Boolean}) public isOwnedByAccount!: boolean | ||
@property({type: String}) public account!: string | ||
@property({type: Boolean}) public horizontal!: boolean | ||
@property({type: Object}) public state!: State | ||
@@ -123,3 +91,3 @@ static get styles() { | ||
} | ||
.asset-detail-price-current { | ||
.asset-detail-price { | ||
font-size: 18px; | ||
@@ -132,9 +100,10 @@ font-weight: 400; | ||
} | ||
.asset-detail-price-current img { | ||
.asset-detail-price img { | ||
margin-left: 5px; | ||
width: 15px; | ||
} | ||
.asset-detail-price-current-value { | ||
.asset-detail-price .value { | ||
margin-left: 5px; | ||
} | ||
.asset-detail-price-previous { | ||
.asset-detail-price .previous-value { | ||
font-size: 14px; | ||
@@ -170,59 +139,12 @@ color: #828282; | ||
} | ||
@property({type: Object}) public asset?: OpenSeaAsset | ||
@property({type: Boolean}) public isOwnedByAccount!: boolean | ||
@property({type: String}) public account!: string | ||
@property({type: Boolean}) public horizontal!: boolean | ||
@property({type: Object}) public state!: State | ||
@property({type: Object}) private lastSaleData?: LastSaleData | ||
public updated(changedProperties: Map<string, string>) { | ||
// Assumption: If the traitData gets updated we should rebuild the | ||
// traits object that populates UI | ||
// Assumption: This will ONLY get called once per refresh | ||
changedProperties.forEach(async (_oldValue: string, propName: string) => { | ||
if (propName === 'asset') { | ||
// Check for a sell order to populate the UI with the sell information | ||
// TODO: We will be using lastSale here once added to SDK | ||
if (this.asset?.sellOrders && this.asset.sellOrders.length > 0) { | ||
const order: Order = this.asset.sellOrders[0] | ||
const paymentToken = order.paymentTokenContract | ||
const decimals = paymentToken ? paymentToken.decimals : TOKEN_DECIMALS // Default decimals to 18 | ||
const currentPrice = order.currentPrice ? +order.currentPrice.toFixed() / Math.pow(10, decimals) : 0 | ||
const expires = new Date(order.expirationTime.toFixed()) | ||
this.lastSaleData = { | ||
paymentToken, | ||
currentPrice, | ||
expires | ||
} | ||
} | ||
// Tell the component to update with new state | ||
await this.requestUpdate() | ||
} | ||
}) | ||
} | ||
public getAssetPriceTemplate() { | ||
// TODO: Needs to account for tokens with images not symbols | ||
// If payment_token.image_url then use token image instead of symbol | ||
let prevPriceTemplate: TemplateResult = html`` | ||
let currentPriceTemplate: TemplateResult = html`` | ||
const sellOrder = this.asset?.sellOrders && this.asset?.sellOrders.length > 0 ? this.asset.sellOrders[0] : null | ||
const currentPriceTemplate = sellOrder && sellOrder?.paymentTokenContract ? | ||
this.getPriceTemplate(PriceType.Current, sellOrder?.paymentTokenContract, sellOrder?.currentPrice?.toNumber() || 0) : null | ||
if (this.lastSaleData?.paymentToken) { | ||
// const currentPriceSymbol = this.lastSaleData.paymentToken.symbol === 'ETH' ? 'Ξ ' : '' | ||
currentPriceTemplate = this.getCurrentPriceTemplate(this.lastSaleData.paymentToken, this.lastSaleData.currentPrice) | ||
const prevPriceTemplate = this.asset?.lastSale?.paymentToken ? | ||
this.getPriceTemplate(PriceType.Previous, this.asset?.lastSale?.paymentToken, | ||
+this.asset?.lastSale?.totalPrice) : null | ||
} | ||
if (this.asset?.lastSale) { | ||
// @ts-ignore ignore until LastSale type gets added to SDK | ||
const formattedPrevPrice = this.asset.lastSale.total_price / Math.pow(10, this.asset.lastSale.payment_token.decimals) | ||
// @ts-ignore ignore until LastSale type gets added to SDK | ||
const prevPriceSymbol = this.asset.lastSale.payment_token.symbol === 'ETH' ? 'Ξ ' : '' | ||
prevPriceTemplate = html`<div class="asset-detail-price-previous">Prev. ${prevPriceSymbol} ${formattedPrevPrice}</div>` | ||
} | ||
return (html` | ||
@@ -299,10 +221,13 @@ <div class="asset-detail-price"> | ||
private getCurrentPriceTemplate(paymentToken: OpenSeaFungibleToken, currentPrice: number) { | ||
private getPriceTemplate(priceType: PriceType, paymentToken: OpenSeaFungibleToken, price: number) { | ||
return html` | ||
<div class="asset-detail-price-current"> | ||
<div class="asset-detail-price"> | ||
${priceType === PriceType.Previous ? html`<div class="previous-value">Prev. </div>` : null} | ||
${ paymentToken.imageUrl ? | ||
html`<img src="${paymentToken.imageUrl}" alt="" ></img>` | ||
: paymentToken.symbol | ||
: html`<div class="previous-value">${paymentToken.symbol === 'ETH' ? 'Ξ' : paymentToken.symbol}</div>` | ||
} | ||
<div class="asset-detail-price-current-value">${currentPrice}</div> | ||
<div class="asset-detail-price value ${priceType}-value"> | ||
${toBaseDenomination(price, paymentToken.decimals)} | ||
</div> | ||
</div> | ||
@@ -326,3 +251,14 @@ ` | ||
private getButtonTemplate() { | ||
return html` | ||
<button | ||
@click="${(e: any) => this.eventHandler(e, 'view')}" | ||
> | ||
${BTN_TEXT[ButtonType.Buy]} | ||
</button> | ||
` | ||
} | ||
// @ts-ignore | ||
private _getButtonTemplate() { | ||
let btnType: ButtonType | ||
@@ -329,0 +265,0 @@ |
import { css, customElement, html, LitElement, property } from 'lit-element' | ||
import { styleMap } from 'lit-html/directives/style-map' | ||
// @ts-ignore ts error TS7016 | ||
@@ -11,3 +10,2 @@ import Web3 from 'web3' | ||
import { NO_WEB3_ERROR } from './constants' | ||
/* lit-element classes */ | ||
@@ -18,17 +16,8 @@ import './pill.ts' | ||
import './nft-card-back.ts' | ||
import { ButtonEvent, CustomWindow } from './types' | ||
import { getProvider, networkFromId } from './utils' | ||
export interface CustomWindow extends Window { | ||
ethereum: Web3.Provider | ||
web3: Web3 | ||
} | ||
declare const window: CustomWindow | ||
export interface ButtonEvent { | ||
detail: { | ||
type: string | ||
} | ||
} | ||
const HORIZONTAL_MIN_CARD_HEIGHT = '190px' | ||
const HORIZONTAL_MIN_CARD_HEIGHT = '200px' | ||
const VERT_MIN_CARD_HEIGHT = '670px' | ||
@@ -41,3 +30,3 @@ | ||
const HORIZONTAL_CARD_HEIGHT = '250px' | ||
const HORIZONTAL_CARD_HEIGHT = '200px' | ||
const HORIZONTAL_CARD_WIDTH = '670px' | ||
@@ -61,2 +50,28 @@ | ||
/* User configurable properties */ | ||
@property({type: Boolean}) public horizontal: boolean = true | ||
@property({type: Boolean}) public orientationMode: OrientationMode = OrientationMode.Auto | ||
@property({type: String}) public tokenAddress: string = '' | ||
@property({type: String}) public contractAddress: string = '' | ||
@property({type: String}) public tokenId: string = '' | ||
@property({type: String}) public width: string = '' | ||
@property({type: String}) public height: string = '' | ||
@property({type: String}) public minHeight: string = '' | ||
@property({type: String}) public network: Network = Network.Main | ||
@property({type: Object}) private asset!: OpenSeaAsset | ||
@property({type: Object}) private traitData: object = {} | ||
@property({type: String}) private account: string = '' | ||
@property({type: String}) private flippedCard: boolean = false | ||
@property({type: Object}) private provider: Web3.Provider | ||
@property({type: Object}) private seaport!: OpenSeaPort | ||
// Card state variables | ||
@property({type: Boolean}) private loading = true | ||
@property({type: Boolean}) private error = false | ||
@property({type: Boolean}) private isOwnedByAccount = false | ||
@property({type: Boolean}) private isUnlocked: boolean = true | ||
@property({type: Boolean}) private hasWeb3: boolean = false | ||
@property({type: Boolean}) private isMatchingNetwork: boolean = false | ||
static get styles() { | ||
@@ -106,47 +121,2 @@ return css` | ||
/* User configurable properties */ | ||
@property({type: Boolean}) public horizontal: boolean = false | ||
@property({type: Boolean}) public orientationMode: OrientationMode = OrientationMode.Auto | ||
@property({type: String}) public contractAddress: string = '' | ||
@property({type: String}) public tokenId: string = '' | ||
@property({type: String}) public width: string = '' | ||
@property({type: String}) public height: string = '' | ||
@property({type: String}) public minHeight: string = '' | ||
@property({type: String}) public network: Network = Network.Main | ||
@property({type: Object}) private asset!: OpenSeaAsset | ||
@property({type: Object}) private traitData: object = {} | ||
@property({type: String}) private account: string = '' | ||
@property({type: String}) private flippedCard: boolean = false | ||
@property({type: Object}) private provider: Web3.Provider | ||
@property({type: Object}) private seaport!: OpenSeaPort | ||
// Card state variables | ||
@property({type: Boolean}) private loading = true | ||
@property({type: Boolean}) private error = false | ||
@property({type: Boolean}) private isOwnedByAccount = false | ||
@property({type: Boolean}) private isUnlocked: boolean = true | ||
@property({type: Boolean}) private hasWeb3: boolean = false | ||
@property({type: Boolean}) private isMatchingNetwork: boolean = false | ||
private static getProvider() { | ||
if (window.ethereum) { | ||
return window.ethereum | ||
} else if (window.web3) { | ||
return window.web3.currentProvider | ||
} else { | ||
return new Web3.providers.HttpProvider('https://mainnet.infura.io') | ||
} | ||
} | ||
// Given the network version this method returns the network name | ||
// Since only Main & Rinkeby are supported we ignore the other networks | ||
private static networkFromId(id: string) { | ||
switch (id) { | ||
case '1': return Network.Main | ||
case '4': return Network.Rinkeby | ||
default: return null | ||
} | ||
} | ||
/** | ||
@@ -159,2 +129,3 @@ * ConnectedCallback - Invoked when a component is added to the document’s DOM. | ||
super.connectedCallback() | ||
this.tokenAddress = this.contractAddress ? this.contractAddress : this.tokenAddress | ||
@@ -181,3 +152,3 @@ let vertCardWidth | ||
// Get the web3 provider | ||
this.provider = NftCard.getProvider() | ||
this.provider = getProvider() | ||
@@ -187,3 +158,4 @@ this.seaport = new OpenSeaPort(this.provider, {networkName: this.network}) | ||
try { | ||
this.asset = await this.seaport.api.getAsset(this.contractAddress, this.tokenId) | ||
this.asset = await this.seaport.api.getAsset({ tokenAddress: this.tokenAddress, tokenId: this.tokenId }) | ||
this.traitData = { | ||
@@ -202,3 +174,3 @@ traits: this.asset.traits, | ||
this.isMatchingNetwork = NftCard.networkFromId(this.provider.networkVersion) === this.network | ||
this.isMatchingNetwork = networkFromId(this.provider.networkVersion) === this.network | ||
@@ -214,3 +186,3 @@ // Tell the component to update with new state | ||
this.provider.on('networkChanged', (networkId: string) => { | ||
const network = NftCard.networkFromId(networkId) | ||
const network = networkFromId(networkId) | ||
this.isMatchingNetwork = network === this.network | ||
@@ -288,2 +260,3 @@ }) | ||
const {detail} = event | ||
switch (detail.type) { | ||
@@ -290,0 +263,0 @@ case 'view': |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
4
-20%30
7.14%7394163
-9.85%7753
-11.16%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated