diff --git a/src/app/@api/collection.api.ts b/src/app/@api/collection.api.ts index ffec82b..25e15ac 100644 --- a/src/app/@api/collection.api.ts +++ b/src/app/@api/collection.api.ts @@ -34,6 +34,10 @@ export class CollectionApi extends BaseApi { super(Dataset.COLLECTION, httpClient); } + public getCollectionById(collectionId: string): Observable { + return this.listen(collectionId); + } + public mintCollection = ( req: Build5Request, ): Observable => this.request(WEN_FUNC.mintCollection, req); diff --git a/src/app/@api/nft.api.ts b/src/app/@api/nft.api.ts index ca4fbe8..2d0bf7c 100644 --- a/src/app/@api/nft.api.ts +++ b/src/app/@api/nft.api.ts @@ -62,6 +62,10 @@ export class NftApi extends BaseApi { public stakeNft = (req: Build5Request): Observable => this.request(WEN_FUNC.stakeNft, req); + public getNftById(nftId: string): Observable { + return this.listen(nftId); + } + public successfullOrders( nftId: string, network?: Network, diff --git a/src/app/@api/order.api.ts b/src/app/@api/order.api.ts index 4ddae44..71784d7 100644 --- a/src/app/@api/order.api.ts +++ b/src/app/@api/order.api.ts @@ -7,6 +7,7 @@ import { WEN_FUNC, Build5Request, NftPurchaseRequest, + NftPurchaseBulkRequest, OrderTokenRequest, AddressValidationRequest, NftBidRequest, @@ -27,6 +28,10 @@ export class OrderApi extends BaseApi { public orderNft = (req: Build5Request): Observable => this.request(WEN_FUNC.orderNft, req); + public orderNfts = ( + req: Build5Request, + ): Observable => this.request(WEN_FUNC.orderNftBulk, req); + public orderToken = ( req: Build5Request, ): Observable => this.request(WEN_FUNC.orderToken, req); diff --git a/src/app/@core/services/filter-storage/filter-storage.service.ts b/src/app/@core/services/filter-storage/filter-storage.service.ts index 8705908..b9a092c 100644 --- a/src/app/@core/services/filter-storage/filter-storage.service.ts +++ b/src/app/@core/services/filter-storage/filter-storage.service.ts @@ -51,6 +51,7 @@ export interface MarketCollectionsFilters { access?: string[]; space?: string[]; category?: string[]; + status?: string[]; }; range?: { price: string; diff --git a/src/app/@core/services/router/router.service.ts b/src/app/@core/services/router/router.service.ts index 529944a..b4e2399 100644 --- a/src/app/@core/services/router/router.service.ts +++ b/src/app/@core/services/router/router.service.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; -import { NavigationEnd, Router } from '@angular/router'; +import { NavigationEnd, Router, Event } from '@angular/router'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { BehaviorSubject } from 'rxjs'; import { DeviceService } from '../device'; +import { filter } from 'rxjs/operators'; @Injectable({ providedIn: 'root', @@ -21,6 +22,12 @@ export class RouterService { public urlToNewToken = '/' + ROUTER_UTILS.config.token.root + '/new'; constructor(private router: Router, private deviceService: DeviceService) { + // this.router.events.pipe( + // filter((event: Event): event is NavigationEnd => event instanceof NavigationEnd) + // ).subscribe((event: NavigationEnd) => { + // console.log('Navigation Event:', event); + // }); + this.updateVariables(); this.router.events.subscribe((obj) => { diff --git a/src/app/@core/utils/local-storage.utils.ts b/src/app/@core/utils/local-storage.utils.ts index a70f86d..7f2722e 100644 --- a/src/app/@core/utils/local-storage.utils.ts +++ b/src/app/@core/utils/local-storage.utils.ts @@ -25,6 +25,7 @@ export enum StorageItem { SelectedTradePriceOption = 'App/selectedTradePriceOption', DepositNftTransaction = 'App/depositNftTransaction-', StakeNftTransaction = 'App/stakeNftTransaction-', + CartItems = 'App/cartItems', } export const getBitItemItem = (nftId: string): unknown | null => { diff --git a/src/app/@shell/ui/header/header.component.html b/src/app/@shell/ui/header/header.component.html index 31c4476..d6222f3 100644 --- a/src/app/@shell/ui/header/header.component.html +++ b/src/app/@shell/ui/header/header.component.html @@ -43,6 +43,24 @@ + + + + + + + + + + Your cart is empty. + + +
+ + +
+ + + diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.less b/src/app/components/cart/components/cart-modal/cart-modal.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/cart/components/cart-modal/cart-modal.component.ts b/src/app/components/cart/components/cart-modal/cart-modal.component.ts new file mode 100644 index 0000000..cb354c4 --- /dev/null +++ b/src/app/components/cart/components/cart-modal/cart-modal.component.ts @@ -0,0 +1,239 @@ +// cart-modal.component.ts +import { + Component, + OnInit, + OnDestroy, + ChangeDetectorRef, + ChangeDetectionStrategy, + EventEmitter, + Output, +} from '@angular/core'; +import { Nft, Collection, MIN_AMOUNT_TO_TRANSFER } from '@build-5/interfaces'; +import { Subscription, forkJoin, map, take, catchError, of } from 'rxjs'; +import { CartService, CartItem } from './../../services/cart.service'; +import { AuthService } from '@components/auth/services/auth.service'; +import { Router } from '@angular/router'; +import { ROUTER_UTILS } from '@core/utils/router.utils'; +import { NzModalService } from 'ng-zorro-antd/modal'; +import { CheckoutOverlayComponent } from '../checkout/checkout-overlay.component'; +import { NftApi } from '@api/nft.api'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; + +@Component({ + selector: 'wen-app-cart-modal', + templateUrl: './cart-modal.component.html', + styleUrls: ['./cart-modal.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CartModalComponent implements OnInit, OnDestroy { + private subscriptions = new Subscription(); + public collectionPath: string = ROUTER_UTILS.config.collection.root; + public nftPath: string = ROUTER_UTILS.config.nft.root; + public cartItemsQuantities: number[] = []; + cartItemPrices: { [key: string]: { originalPrice: number; discountedPrice: number } } = {}; + isCartCheckoutOpen = false; + + constructor( + public cartService: CartService, + private cd: ChangeDetectorRef, + public auth: AuthService, + private modalService: NzModalService, + private nftApi: NftApi, + private notification: NzNotificationService, + private router: Router, + ) {} + + ngOnInit() { + this.subscriptions.add( + this.cartService.getCartItems().subscribe((items) => { + // console.log('[CartModalComponent-ngOnInit] Cart items updated:', items); + this.cartItemsStatus = items.map((item) => this.cartItemStatus(item)); + this.cartItemsQuantities = items.map((item) => this.cartItemSaleAvailableQty(item)); + items.forEach((item) => { + const originalPrice = this.calcPrice(item, 1); + const discountedPrice = this.calcPrice(item, this.discount(item.collection, item.nft)); + this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; + }); + }), + ); + + this.subscriptions.add( + this.cartService.showCart$.subscribe((show) => { + if (show) { + this.refreshCartData(); + } + }), + ); + } + + cartItemsStatus: string[] = []; + + trackByItemId(index: number, item: CartItem): string { + return item.nft.uid; + } + + public removeFromCart(itemId: string): void { + this.cartService.removeFromCart(itemId); + } + + private refreshCartData() { + // console.log('Refreshing cart items...'); + const cartItems = this.cartService.getCartItems().getValue(); + // console.log('Current cart items:', cartItems); + + const freshDataObservables = cartItems.map((item) => + this.nftApi.getNftById(item.nft.uid).pipe( + take(1), + map((freshNft) => { + // console.log(`Fetched fresh data for NFT ${item.nft.uid}:`, freshNft); + return freshNft ? { ...item, nft: freshNft } : item; + }), + catchError((error) => { + // console.error(`Error fetching fresh data for NFT ${item.nft.uid}:`, error); + return of(item); + }), + ), + ); + + forkJoin(freshDataObservables).subscribe( + (freshCartItems) => { + // console.log('Fresh cart items:', freshCartItems); + + this.cartService.updateCartItems(freshCartItems); + + this.cartItemsStatus = freshCartItems.map((item) => this.cartItemStatus(item)); + this.cartItemsQuantities = freshCartItems.map((item) => + this.cartItemSaleAvailableQty(item), + ); + freshCartItems.forEach((item) => { + const originalPrice = this.calcPrice(item, 1); + const discountedPrice = this.calcPrice(item, this.discount(item.collection, item.nft)); + this.cartItemPrices[item.nft.uid] = { originalPrice, discountedPrice }; + }); + + // console.log('Finished refreshing cart items.'); + + this.cd.markForCheck(); + }, + (error) => { + console.error('Error while refreshing cart items: ', error); + this.notification.error($localize`Error while refreshing cart items: ` + error, ''); + }, + ); + } + + public updateQuantity(event: Event, itemId: string): void { + const inputElement = event.target as HTMLInputElement; + const newQuantity = Number(inputElement.value); + + if (newQuantity === 0) { + this.cartService.removeFromCart(itemId); + } else { + const cartItems = this.cartService.getCartItems().getValue(); + const itemIndex = cartItems.findIndex((cartItem) => cartItem.nft.uid === itemId); + if (itemIndex !== -1) { + cartItems[itemIndex].quantity = newQuantity; + this.cartService.saveCartItems(); + } + } + } + + public discount(collection?: Collection | null, nft?: Nft | null): number { + if (!collection?.space || !this.auth.member$.value || nft?.owner) { + return 1; + } + + const spaceRewards = (this.auth.member$.value.spaces || {})[collection.space]; + const descDiscounts = [...(collection.discounts || [])].sort((a, b) => b.amount - a.amount); + for (const discount of descDiscounts) { + const awardStat = (spaceRewards?.awardStat || {})[discount.tokenUid!]; + const memberTotalReward = awardStat?.totalReward || 0; + if (memberTotalReward >= discount.tokenReward) { + return 1 - discount.amount; + } + } + return 1; + } + + public calc(amount: number | null | undefined, discount: number): number { + let finalPrice = Math.ceil((amount || 0) * discount); + if (finalPrice < MIN_AMOUNT_TO_TRANSFER) { + finalPrice = MIN_AMOUNT_TO_TRANSFER; + } + + return finalPrice; + } + + public calcPrice(item: CartItem, discount: number): number { + const itemPrice = item.nft?.availablePrice || item.nft?.price || 0; + return this.calc(itemPrice, discount); + } + + public cartItemStatus(item: CartItem): any { + // console.log("[cart-modal.component-cartItemStatus] function called"); + const itemAvailable = this.cartService.isCartItemAvailableForSale(item); + if (itemAvailable) { + // console.log("[cart-modal.component-cartItemStatus] returning Available, itemAvailable: " + itemAvailable); + return 'Available'; + } + // console.log("[cart-modal.component-cartItemStatus] returning Not Available, itemAvailable: " + itemAvailable); + return 'Not Available'; + } + + private cartItemSaleAvailableQty(item: CartItem): number { + // console.log("[cart-modal.component-cartItemSaleAvailableQty] function called"); + const availQty = this.cartService.getAvailableNftQuantity(item); + // console.log("[cart-modal.component] cartItemSaleAvailableQty, qty: " + availQty); + return availQty; + } + + public handleClose(): void { + this.cartService.hideCart(); + } + + public goToNft(nftUid: string): void { + if (!nftUid) { + console.error('No NFT UID provided.'); + return; + } + + this.router.navigate(['/', this.nftPath, nftUid]); + this.cartService.hideCart(); + } + + public goToCollection(colUid: string): void { + if (!colUid) { + console.error('No Collection UID provided.'); + return; + } + + this.router.navigate(['/', this.collectionPath, colUid]); + this.cartService.hideCart(); + } + + public handleCartCheckout(): void { + const cartItems = this.cartService.getCartItems().getValue(); + + const modalRef = this.modalService.create({ + nzTitle: 'Checkout', + nzContent: CheckoutOverlayComponent, + nzComponentParams: { items: cartItems }, + nzFooter: null, + nzWidth: '80%', + }); + + modalRef.afterClose.subscribe(() => { + // this.cartService.hideCart(); + }); + } + + public handleCloseCartCheckout(alsoCloseCartModal: boolean): void { + if (alsoCloseCartModal) { + this.cartService.hideCart(); + } + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } +} diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.html b/src/app/components/cart/components/checkout/checkout-overlay.component.html new file mode 100644 index 0000000..a1be5fd --- /dev/null +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.html @@ -0,0 +1,292 @@ + +
+ Notice {{ unavailableItemCount }} items were not included in + the checkout due to not being available for sale. +
+ +
+

Network/Token: {{ group.tokenSymbol }}

+ + + + + NFT Name + Collection Name + Quantity Added + Price ({{ group.tokenSymbol }}) + Line Item Total + + + + + + + + {{ item.nft.name }} + + + + + {{ item.collection.name }} + + + {{ item.quantity }} + + {{ + item.salePrice + | formatToken + : (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) + : true + : true + | async + }} + + + {{ + item.quantity * item.salePrice + | formatToken + : (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) + : true + : true + | async + }} + + + + + + Total + + {{ group.totalQuantity }} + + + + + {{ group.totalPrice | formatToken : group.network : true : true | async }} + + + + + + +
+ +
+ + + + + + + + + + + + + + + +
+
+ + + +
+ +
+
+ + +
+ + + + + +
+ Your NFTs will be locked for purchase for {{ lockTime }} + minutes. +
+
+
+ + +
+
+ +
+ {{ + expiryTicker$ | async | countdownTime + }} + + remaining to make the transfer. + Expired +
+
+ + + + + + + + +
+
+ + +
+
+
Transaction history
+ +
+
+
{{ t.date | Time }}
+ {{ t.label }} +
{{ t.label }}
+
+
+
+ + +
+
+ + +
+
+
Transaction history
+ +
+
+
{{ t.date | Time }}
+ {{ t.label }} +
{{ t.label }}
+
+
+
+ +
+
+ +
+
Transaction complete. Congratulations.
+
+ +
+ +
+
+
diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.less b/src/app/components/cart/components/checkout/checkout-overlay.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/cart/components/checkout/checkout-overlay.component.ts b/src/app/components/cart/components/checkout/checkout-overlay.component.ts new file mode 100644 index 0000000..a1ded8b --- /dev/null +++ b/src/app/components/cart/components/checkout/checkout-overlay.component.ts @@ -0,0 +1,556 @@ +import { + Component, + Input, + OnInit, + ChangeDetectorRef, + ChangeDetectionStrategy, + Output, + EventEmitter, +} from '@angular/core'; +import { CartItem, CartService } from './../../services/cart.service'; +import { + CollectionType, + Nft, + Timestamp, + Transaction, + TransactionType, + TRANSACTION_AUTO_EXPIRY_MS, + NftPurchaseRequest, + Network, +} from '@build-5/interfaces'; +import { AuthService } from '@components/auth/services/auth.service'; +import { NotificationService } from '@core/services/notification'; +import { OrderApi } from '@api/order.api'; +import { NftApi } from '@api/nft.api'; +// import { FileApi } from '@api/file.api'; +import { BehaviorSubject, firstValueFrom, interval, Subscription } from 'rxjs'; +import { TransactionService } from '@core/services/transaction'; +import { getItem, removeItem, setItem, StorageItem } from '@core/utils'; +import dayjs from 'dayjs'; +import { HelperService } from '@pages/nft/services/helper.service'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { ROUTER_UTILS } from '@core/utils/router.utils'; +import { Router } from '@angular/router'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; +import { NzModalRef } from 'ng-zorro-antd/modal'; +import { ThemeList, ThemeService } from '@core/services/theme'; + +export enum StepType { + CONFIRM = 'Confirm', + TRANSACTION = 'Transaction', + WAIT = 'Wait', + COMPLETE = 'Complete', +} + +interface GroupedCartItem { + tokenSymbol: string; + items: CartItem[]; + totalQuantity: number; + totalPrice: number; + network: Network | undefined; +} + +interface HistoryItem { + uniqueId: string; + date: dayjs.Dayjs | Timestamp | null; + label: string; + transaction?: Transaction; + link?: string; +} + +@UntilDestroy() +@Component({ + selector: 'wen-app-checkout-overlay', + templateUrl: './checkout-overlay.component.html', + styleUrls: ['./checkout-overlay.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CheckoutOverlayComponent implements OnInit { + @Input() currentStep = StepType.CONFIRM; + @Input() items: CartItem[] = []; + @Input() set isOpen(value: boolean) { + this._isOpen = value; + // this.checkoutService.modalOpen$.next(value); + } + + @Output() wenOnClose = new EventEmitter(); + @Output() wenOnCloseCartCheckout = new EventEmitter(); + groupedCartItems: GroupedCartItem[] = []; + unavailableItemCount = 0; + cartItemPrices: { [key: string]: { originalPrice: number; discountedPrice: number } } = {}; + public agreeTermsConditions = false; + public transaction$: BehaviorSubject = new BehaviorSubject< + Transaction | undefined + >(undefined); + public history: HistoryItem[] = []; + public expiryTicker$: BehaviorSubject = + new BehaviorSubject(null); + public invalidPayment = false; + public receivedTransactions = false; + public targetAddress?: string; + public targetAmount?: number; + public purchasedNfts?: Nft[] | null; + private _isOpen = false; + public stepType = StepType; + public selectedNetwork: string | null = null; + public mintingDataNetwork: Network | undefined; + formattedTotalPrice = ''; + private purchasedTokenSymbol: string | null = null; + + private transSubscription?: Subscription; + public nftPath = ROUTER_UTILS.config.nft.root; + public collectionPath: string = ROUTER_UTILS.config.collection.root; + + public theme = ThemeList; + constructor( + private cartService: CartService, + private auth: AuthService, + private notification: NotificationService, + private orderApi: OrderApi, + public transactionService: TransactionService, + public helper: HelperService, + private cd: ChangeDetectorRef, + private nftApi: NftApi, + private router: Router, + private nzNotification: NzNotificationService, + private modalRef: NzModalRef, + public themeService: ThemeService, + ) {} + + public get themes(): typeof ThemeList { + return ThemeList; + } + + ngOnInit() { + // console.log('checkout-overlay ngOnInit called, running groupItems code.'); + this.groupItems(); + this.receivedTransactions = false; + const listeningToTransaction: string[] = []; + this.transaction$.pipe(untilDestroyed(this)).subscribe((val) => { + // console.log('transaction val: ', val); + if (val && val.type === TransactionType.ORDER) { + this.targetAddress = val.payload.targetAddress; + this.targetAmount = val.payload.amount; + const expiresOn: dayjs.Dayjs = dayjs(val.payload.expiresOn!.toDate()); + if (expiresOn.isBefore(dayjs()) || val.payload?.void || val.payload?.reconciled) { + // It's expired. + removeItem(StorageItem.CheckoutTransaction); + } + if (val.linkedTransactions && val.linkedTransactions?.length > 0) { + this.currentStep = StepType.WAIT; + // Listen to other transactions. + for (const tranId of val.linkedTransactions) { + if (listeningToTransaction.indexOf(tranId) > -1) { + continue; + } + + listeningToTransaction.push(tranId); + this.orderApi + .listen(tranId) + .pipe(untilDestroyed(this)) + .subscribe(this.transaction$); + } + } else if (!val.linkedTransactions || val.linkedTransactions.length === 0) { + this.currentStep = StepType.TRANSACTION; + } + + this.expiryTicker$.next(expiresOn); + } + + if (val && val.type === TransactionType.PAYMENT && val.payload.reconciled === true) { + this.pushToHistory( + val, + val.uid + '_payment_received', + val.createdOn, + $localize`Payment received.`, + (val).payload?.chainReference, + ); + } + + if ( + val && + val.type === TransactionType.PAYMENT && + val.payload.reconciled === true && + (val).payload.invalidPayment === false + ) { + // Let's add delay to achive nice effect. + setTimeout(() => { + this.pushToHistory( + val, + val.uid + '_confirming_trans', + dayjs(), + $localize`Confirming transaction.`, + ); + }, 1000); + + setTimeout(() => { + this.pushToHistory( + val, + val.uid + '_confirmed_trans', + dayjs(), + $localize`Transaction confirmed.`, + ); + this.receivedTransactions = true; + this.currentStep = StepType.COMPLETE; + + // console.log('[checkout-overlay.component-purchase] transaction after purchase complete: ', val); + this.removePurchasedGroupItems(); + + this.cd.markForCheck(); + }, 2000); + + // Load purchased NFTs. + if (val.payload.nftOrders && val.payload.nftOrders.length > 0) { + this.purchasedNfts = this.purchasedNfts || []; + val.payload.nftOrders.forEach((nftOrder) => { + firstValueFrom(this.nftApi.listen(nftOrder.nft)).then((obj) => { + if (obj !== null && obj !== undefined) { + const purchasedNft = obj as Nft; + + this.purchasedNfts = [...(this.purchasedNfts || []), purchasedNft]; + this.cd.markForCheck(); + } + }); + }); + } + } + + if ( + val && + val.type === TransactionType.CREDIT && + val.payload.reconciled === true && + val.ignoreWallet === false && + !val.payload?.walletReference?.chainReference + ) { + this.pushToHistory( + val, + val.uid + '_false', + val.createdOn, + $localize`Invalid amount received. Refunding transaction...`, + ); + } + + const markInvalid = () => { + setTimeout(() => { + this.currentStep = StepType.TRANSACTION; + this.invalidPayment = true; + this.cd.markForCheck(); + }, 2000); + }; + + if ( + val && + val.type === TransactionType.CREDIT && + val.payload.reconciled === true && + val.ignoreWallet === true && + !val.payload?.walletReference?.chainReference + ) { + this.pushToHistory( + val, + val.uid + '_false', + val.createdOn, + $localize`Invalid transaction.You must gift storage deposit.`, + ); + markInvalid(); + } + + if ( + val && + val.type === TransactionType.CREDIT && + val.payload.reconciled === true && + val.payload?.walletReference?.chainReference + ) { + this.pushToHistory( + val, + val.uid + '_true', + dayjs(), + $localize`Invalid payment refunded.`, + val.payload?.walletReference?.chainReference, + ); + + // Let's go back to wait. With slight delay so they can see this. + markInvalid(); + } + + this.cd.markForCheck(); + }); + + if (getItem(StorageItem.CheckoutTransaction)) { + this.transSubscription = this.orderApi + .listen(getItem(StorageItem.CheckoutTransaction)) + .subscribe(this.transaction$); + } + + // Run ticker. + const int: Subscription = interval(1000) + .pipe(untilDestroyed(this)) + .subscribe(() => { + this.expiryTicker$.next(this.expiryTicker$.value); + + // If it's in the past. + if (this.expiryTicker$.value) { + const expiresOn: dayjs.Dayjs = dayjs(this.expiryTicker$.value).add( + TRANSACTION_AUTO_EXPIRY_MS, + 'ms', + ); + if (expiresOn.isBefore(dayjs())) { + this.expiryTicker$.next(null); + removeItem(StorageItem.CheckoutTransaction); + int.unsubscribe(); + this.reset(); + } + } + }); + } + + groupItems() { + // console.log('groupItems function called.') + const groups: { [tokenSymbol: string]: GroupedCartItem } = {}; + this.items.forEach((item) => { + const tokenSymbol = + (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) || 'Unknown'; + const discount = this.discount(item); + const originalPrice = this.calcPrice(item, 1); + const discountedPrice = this.calcPrice(item, discount); + const price = this.discount(item) < 1 ? discountedPrice : originalPrice; + item.salePrice = price; + + const network = + (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) || undefined; + + if (this.cartService.isCartItemAvailableForSale(item)) { + if (!groups[tokenSymbol]) { + groups[tokenSymbol] = { + tokenSymbol, + items: [], + totalQuantity: 0, + totalPrice: 0, + network, + }; + } + groups[tokenSymbol].items.push(item); + groups[tokenSymbol].totalQuantity += item.quantity; + groups[tokenSymbol].totalPrice += item.quantity * item.salePrice; + } else { + this.unavailableItemCount++; + } + + // console.log('Cart item loop finished, group: ', groups[tokenSymbol]) + }); + + this.groupedCartItems = Object.values(groups); + + if (this.groupedCartItems.length === 1) { + this.selectedNetwork = this.groupedCartItems[0].tokenSymbol; + } + } + + private removePurchasedGroupItems(): void { + if (this.purchasedTokenSymbol) { + this.cartService.removeGroupItemsFromCart(this.purchasedTokenSymbol); + this.purchasedTokenSymbol = null; + } + } + + private calcPrice(item: CartItem, discount: number): number { + return this.cartService.calcPrice(item, discount); + } + + private discount(item: CartItem): number { + return this.cartService.discount(item.collection, item.nft); + } + + public isCartItemAvailableForSale(item: CartItem): any { + return this.cartService.isCartItemAvailableForSale(item); + } + + public reset(): void { + this.receivedTransactions = false; + this.isOpen = false; + this.currentStep = StepType.CONFIRM; + this.purchasedNfts = undefined; + this.cd.markForCheck(); + } + + public close(alsoCloseCartModal = false): void { + this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); + this.modalRef.close(); + } + + public goToNft(nftUid: string, alsoCloseCartModal = false): void { + if (!nftUid) { + console.error('No NFT UID provided.'); + return; + } + this.router.navigate(['/', this.nftPath, nftUid]); + this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); + this.modalRef.close(); + } + + public goToCollection(colUid: string, alsoCloseCartModal = false): void { + if (!colUid) { + console.error('No Collection UID provided.'); + return; + } + this.router.navigate(['/', this.collectionPath, colUid]); + this.wenOnCloseCartCheckout.emit(alsoCloseCartModal); + this.modalRef.close(); + } + + public getRecords(): Nft[] | null | undefined { + return this.purchasedNfts || null; + } + + public pushToHistory( + transaction: Transaction, + uniqueId: string, + date?: dayjs.Dayjs | Timestamp | null, + text?: string, + link?: string, + ): void { + if ( + this.history.find((s) => { + return s.uniqueId === uniqueId; + }) + ) { + return; + } + + if (date && text) { + this.history.unshift({ + transaction, + uniqueId: uniqueId, + date: date, + label: text, + link: link, + }); + } + } + + public get lockTime(): number { + return TRANSACTION_AUTO_EXPIRY_MS / 1000 / 60; + } + + public async initiateBulkOrder(): Promise { + const selectedGroup = this.groupedCartItems.find( + (group) => group.tokenSymbol === this.selectedNetwork, + ); + + if (selectedGroup && selectedGroup.items.length > 0) { + this.purchasedTokenSymbol = selectedGroup.tokenSymbol; + const firstItem = selectedGroup.items[0]; + this.mintingDataNetwork = firstItem.nft?.placeholderNft + ? firstItem.collection?.mintingData?.network + : firstItem.nft?.mintingData?.network; + } + + if (!selectedGroup || selectedGroup.items.length === 0) { + console.warn('No network selected or no items in the selected network.'); + this.nzNotification.error( + $localize`No network selected or no items in the selected network.`, + '', + ); + return; + } + + const nfts = this.convertGroupedCartItemsToNfts(selectedGroup); + + if (nfts.length === 0) { + console.warn('No NFTs to purchase.'); + this.nzNotification.error($localize`No NFTs to purchase.`, ''); + return; + } + + await this.proceedWithBulkOrder(nfts); + } + + public convertGroupedCartItemsToNfts(selectedGroup: GroupedCartItem): NftPurchaseRequest[] { + const nfts: NftPurchaseRequest[] = []; + + selectedGroup.items.forEach((item) => { + if (item.nft && item.collection) { + // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] looped nft (item): ', item); + + for (let i = 0; i < item.quantity; i++) { + // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] creating bulk order NftPurchaseRequest for item, qty#: ', i) + const nftData: NftPurchaseRequest = { + collection: item.collection.uid, + }; + + if (item.nft.owner || item.collection.type === CollectionType.CLASSIC) { + // console.log('[checkout-overlay.component-convertGroupedCartItemsToNfts] passed item.nft.owner || item.collection.type === CollectionType.CLASSIC. item.nft.owner: ' + item.nft.owner + '. item.collection.type: ' + item.collection.type + '. item.nft.uid: ' + item.nft.uid); + nftData.nft = item.nft.uid; + } + + nfts.push(nftData); + } + } + }); + + return nfts; + } + + public async proceedWithBulkOrder(nfts: NftPurchaseRequest[]): Promise { + // console.log('[checkout-overlay.component-proceddWithBulkOrder] nfts passed in: ', nfts) + const selectedGroup = this.groupedCartItems.find( + (group) => group.tokenSymbol === this.selectedNetwork, + ); + if (!selectedGroup) { + console.warn('No network selected or no items in the selected network.'); + this.nzNotification.error( + $localize`No network selected or no items in the selected network.`, + '', + ); + return; + } + + if (nfts.length === 0 || !this.agreeTermsConditions) { + console.warn('No NFTs to purchase or terms and conditions are not agreed.'); + this.nzNotification.error( + $localize`No NFTs to purchase or terms and conditions are not agreed.`, + '', + ); + return; + } + + const bulkPurchaseRequest = { + orders: nfts, + }; + + // console.log('[checkout-overlay.component-proceddWithBulkOrder] params being passed for signing: ', bulkPurchaseRequest); + + await this.auth.sign(bulkPurchaseRequest, async (signedRequest, finish) => { + this.notification + .processRequest( + this.orderApi.orderNfts(signedRequest), + $localize`Bulk order created.`, + finish, + ) + .subscribe((transaction: Transaction | undefined) => { + if (transaction) { + this.transSubscription?.unsubscribe(); + setItem(StorageItem.CheckoutTransaction, transaction.uid); + this.transSubscription = this.orderApi + .listen(transaction.uid) + .subscribe(this.transaction$); + this.pushToHistory( + transaction, + transaction.uid, + dayjs(), + $localize`Waiting for transaction...`, + ); + } else { + console.error('Transaction failed or did not return a valid transaction.'); + this.nzNotification.error( + $localize`Transaction failed or did not return a valid transaction.`, + '', + ); + } + }); + }); + } +} diff --git a/src/app/components/cart/services/cart.service.ts b/src/app/components/cart/services/cart.service.ts new file mode 100644 index 0000000..01abd0a --- /dev/null +++ b/src/app/components/cart/services/cart.service.ts @@ -0,0 +1,181 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { Nft, Collection, MIN_AMOUNT_TO_TRANSFER } from '@build-5/interfaces'; +import { getItem, setItem, StorageItem } from './../../../@core/utils/local-storage.utils'; +import { NzNotificationService } from 'ng-zorro-antd/notification'; +import { HelperService } from '@pages/nft/services/helper.service'; +import { AuthService } from '@components/auth/services/auth.service'; + +export interface CartItem { + nft: Nft; + collection: Collection; + quantity: number; + salePrice: number; +} + +@Injectable({ + providedIn: 'root', +}) +export class CartService { + private showCartSubject = new BehaviorSubject(false); + public showCart$ = this.showCartSubject.asObservable(); + private cartItemsSubject = new BehaviorSubject(this.loadCartItems()); + + constructor( + private notification: NzNotificationService, + private helperService: HelperService, + public auth: AuthService, + ) { + // console.log('CartService instance created'); + } + + public showCart(): void { + this.showCartSubject.next(true); + } + + public hideCart(): void { + this.showCartSubject.next(false); + } + + public refreshCartItems(): void { + this.cartItemsSubject.next(this.cartItemsSubject.value); + } + + public addToCart(cartItem: CartItem): void { + console.log('[CartService] addToCart function called. Adding cart item: ', cartItem); + const currentItems = this.cartItemsSubject.value; + + const isItemAlreadyInCart = currentItems.some((item) => item.nft.uid === cartItem.nft.uid); + + if (!isItemAlreadyInCart) { + const updatedCartItems = [...currentItems, cartItem]; + this.cartItemsSubject.next(updatedCartItems); + this.saveCartItems(); + this.notification.success( + $localize`NFT ` + + cartItem.nft.name + + ` from collection ` + + cartItem.collection.name + + ` has been added to your cart.`, + '', + ); + // console.log('[CartService] NFT added to cart:', cartItem); + } else { + // console.log('[CartService] NFT is already in the cart:', cartItem); + this.notification.error($localize`This NFT already exists in your cart.`, ''); + } + } + + public removeFromCart(itemId: string): void { + // console.log('[CartService] removeFromCart function called.'); + const updatedCartItems = this.cartItemsSubject.value.filter((item) => item.nft.uid !== itemId); + this.cartItemsSubject.next(updatedCartItems); + this.saveCartItems(); + // console.log('[CartService-removeFromCart] Cart updated:', updatedCartItems); + } + + public removeItemsFromCart(itemIds: string[]): void { + const updatedCartItems = this.cartItemsSubject.value.filter( + (item) => !itemIds.includes(item.nft.uid), + ); + this.cartItemsSubject.next(updatedCartItems); + this.saveCartItems(); + } + + public removeGroupItemsFromCart(tokenSymbol: string): void { + const updatedCartItems = this.cartItemsSubject.value.filter((item) => { + const itemTokenSymbol = + (item.nft?.placeholderNft + ? item.collection?.mintingData?.network + : item.nft?.mintingData?.network) || 'Unknown'; + return itemTokenSymbol !== tokenSymbol; + }); + this.cartItemsSubject.next(updatedCartItems); + this.saveCartItems(); + } + + public getCartItems(): BehaviorSubject { + // console.log('[CartService] getCartItems function called.'); + return this.cartItemsSubject; + } + + public updateCartItems(updatedItems: CartItem[]): void { + this.cartItemsSubject.next(updatedItems); // Update the BehaviorSubject with the new cart items + this.saveCartItems(); // Save the updated cart items to local storage or your backend + } + + public saveCartItems(): void { + // console.log('[CartService] getCartItems function called.'); + setItem(StorageItem.CartItems, this.cartItemsSubject.value); + // console.log('[CartService] Saving cart items to local storage:', this.cartItemsSubject.value); + } + + private loadCartItems(): CartItem[] { + // console.log('[CartService] Loading cart items from local storage'); + const items = getItem(StorageItem.CartItems) as CartItem[]; + // console.log('[CartService] Cart items loaded from local storage:', items); + return items || []; + } + + public isNftAvailableForSale(nft: Nft, collection: Collection): boolean { + const isLocked = this.helperService.isLocked(nft, collection, true); + const isOwner = nft.owner === this.auth.member$.value?.uid; + const availableForSale = this.helperService.isAvailableForSale(nft, collection); + // console.log(`[cart.service-isNftAvailableForSale] results for NFT ${nft.name}; availableForSale: ${availableForSale}, isLocked: ${isLocked}, isOwner: ${isOwner}`); + + return !isLocked && availableForSale && (!isOwner || !nft.owner); + } + + public isCartItemAvailableForSale(cartItem: CartItem): boolean { + return this.isNftAvailableForSale(cartItem.nft, cartItem.collection); + } + + public getAvailableNftQuantity(cartItem: CartItem): number { + const isAvailableForSale = this.helperService.isAvailableForSale( + cartItem.nft, + cartItem.collection, + ); + // console.log("[cart.service-getAvailableNftQuantity] function called for cartItem.nft.name: " + cartItem.nft.name + ", isAvailableForSale: " + isAvailableForSale); + + if (cartItem.nft.placeholderNft && isAvailableForSale) { + // console.log("[service-getAvailableNftQuantity] returning qty, placeholder: " + cartItem.nft.placeholderNft + ". availableNfts " + cartItem.collection.availableNfts); + return cartItem.collection.availableNfts || 0; + } else if (isAvailableForSale) { + // console.log("[service-getAvailableNftQuantity] returning 1, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); + return 1; + } + // console.log("[service-getAvailableNftQuantity] returning 0, isAvailableForSale: " + isAvailableForSale + ". placeholder: " + cartItem.nft.placeholderNft); + return 0; + } + + public discount(collection?: Collection | null, nft?: Nft | null): number { + if (!collection?.space || !this.auth.member$.value || nft?.owner) { + return 1; + } + + const spaceRewards = (this.auth.member$.value.spaces || {})[collection.space]; + const descDiscounts = [...(collection.discounts || [])].sort((a, b) => b.amount - a.amount); + for (const discount of descDiscounts) { + const awardStat = (spaceRewards?.awardStat || {})[discount.tokenUid!]; + const memberTotalReward = awardStat?.totalReward || 0; + if (memberTotalReward >= discount.tokenReward) { + return 1 - discount.amount; + } + } + return 1; + } + + public calc(amount: number | null | undefined, discount: number): number { + let finalPrice = Math.ceil((amount || 0) * discount); + if (finalPrice < MIN_AMOUNT_TO_TRANSFER) { + finalPrice = MIN_AMOUNT_TO_TRANSFER; + } + + return finalPrice; + } + + public calcPrice(item: CartItem, discount: number): number { + const itemPrice = item.nft?.availablePrice || item.nft?.price || 0; + return this.calc(itemPrice, discount); // assuming calc method applies the discount + } +} diff --git a/src/app/components/icon/cart/cart.component.html b/src/app/components/icon/cart/cart.component.html new file mode 100644 index 0000000..4c29236 --- /dev/null +++ b/src/app/components/icon/cart/cart.component.html @@ -0,0 +1,11 @@ + + + + + diff --git a/src/app/components/icon/cart/cart.component.less b/src/app/components/icon/cart/cart.component.less new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/icon/cart/cart.component.ts b/src/app/components/icon/cart/cart.component.ts new file mode 100644 index 0000000..1886bfa --- /dev/null +++ b/src/app/components/icon/cart/cart.component.ts @@ -0,0 +1,9 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'wen-icon-cart', + templateUrl: './cart.component.html', + styleUrls: ['./cart.component.less'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CartIconComponent {} diff --git a/src/app/components/icon/icon.module.ts b/src/app/components/icon/icon.module.ts index dc57b80..4098511 100644 --- a/src/app/components/icon/icon.module.ts +++ b/src/app/components/icon/icon.module.ts @@ -12,6 +12,7 @@ import { AssemblyIconComponent } from './assembly/assembly.component'; import { AwardIconComponent } from './award/award.component'; import { BellIconComponent } from './bell/bell.component'; import { CalendarIconComponent } from './calendar/calendar.component'; +import { CartIconComponent } from './cart/cart.component'; import { CheckCircleIconComponent } from './check-circle/check-circle.component'; import { CheckIconComponent } from './check/check.component'; import { CloseIconComponent } from './close/close.component'; @@ -187,6 +188,7 @@ import { WalletIconComponent } from './wallet/wallet.component'; SpecificMembersOnlyIconComponent, SadCryIconComponent, LinkBrokenIconComponent, + CartIconComponent, ], exports: [ GlobeIconComponent, @@ -279,6 +281,7 @@ import { WalletIconComponent } from './wallet/wallet.component'; SpecificMembersOnlyIconComponent, SadCryIconComponent, LinkBrokenIconComponent, + CartIconComponent, ], imports: [CommonModule], }) diff --git a/src/app/components/nft/components/nft-card/nft-card.component.html b/src/app/components/nft/components/nft-card/nft-card.component.html index 49f610d..1120b0f 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.html +++ b/src/app/components/nft/components/nft-card/nft-card.component.html @@ -239,6 +239,24 @@ + + + diff --git a/src/app/components/nft/components/nft-card/nft-card.component.ts b/src/app/components/nft/components/nft-card/nft-card.component.ts index 3c70eb3..72a82a6 100644 --- a/src/app/components/nft/components/nft-card/nft-card.component.ts +++ b/src/app/components/nft/components/nft-card/nft-card.component.ts @@ -10,6 +10,7 @@ import { UnitsService } from '@core/services/units'; import { ROUTER_UTILS } from '@core/utils/router.utils'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { HelperService } from '@pages/nft/services/helper.service'; +import { CartService } from '@components/cart/services/cart.service'; import { Access, Collection, @@ -89,6 +90,7 @@ export class NftCardComponent { private memberApi: MemberApi, private fileApi: FileApi, private cache: CacheService, + public cartService: CartService, ) {} public onBuy(event: MouseEvent): void { @@ -182,4 +184,22 @@ export class NftCardComponent { public get collectionStatuses(): typeof CollectionStatus { return CollectionStatus; } + + public addToCart( + event: MouseEvent, + nft: Nft | null | undefined, + collection: Collection | null | undefined, + ): void { + event.stopPropagation(); + event.preventDefault(); + + if (nft && collection) { + console.log('[NftCardComponent] Adding item to cart:', nft, collection); + this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); + // Optionally, provide feedback to the user + } else { + // Handle the case when nft or collection is null or undefined + console.error('Attempted to add a null or undefined NFT or Collection to the cart'); + } + } } diff --git a/src/app/components/nft/components/nft-checkout/nft-checkout.component.ts b/src/app/components/nft/components/nft-checkout/nft-checkout.component.ts index ac136ea..1c161b0 100644 --- a/src/app/components/nft/components/nft-checkout/nft-checkout.component.ts +++ b/src/app/components/nft/components/nft-checkout/nft-checkout.component.ts @@ -432,10 +432,19 @@ export class NftCheckoutComponent implements OnInit, OnDestroy { params.nft = this.nft.uid; } + console.log( + '[nft-checkout.component-proceedWithOrder] params being passed for signing: ', + params, + ); + await this.auth.sign(params, (sc, finish) => { this.notification .processRequest(this.orderApi.orderNft(sc), $localize`Order created.`, finish) .subscribe((val: any) => { + console.log( + '[nft-this.checkoutService.component-proceedWithOrder] val after processing: ', + val, + ); this.transSubscription?.unsubscribe(); setItem(StorageItem.CheckoutTransaction, val.uid); this.transSubscription = this.orderApi.listen(val.uid).subscribe(this.transaction$); diff --git a/src/app/pages/award/pages/new/new.page.ts b/src/app/pages/award/pages/new/new.page.ts index c8a5f8d..700132b 100644 --- a/src/app/pages/award/pages/new/new.page.ts +++ b/src/app/pages/award/pages/new/new.page.ts @@ -17,6 +17,8 @@ import { TEST_AVAILABLE_MINTABLE_NETWORKS, Token, TokenStatus, + getDefDecimalIfNotSet, + Network, } from '@build-5/interfaces'; import { BehaviorSubject, of, Subscription, switchMap } from 'rxjs'; import { filter, map } from 'rxjs/operators'; @@ -27,7 +29,7 @@ import { NotificationService } from './../../../../@core/services/notification/n import { AuthService } from './../../../../components/auth/services/auth.service'; import { FileApi } from '@api/file.api'; -import { getDefDecimalIfNotSet, Network } from '@build-5/interfaces'; + import { NzNotificationService } from 'ng-zorro-antd/notification'; import { NzUploadChangeParam, NzUploadFile, NzUploadXHRArgs } from 'ng-zorro-antd/upload'; diff --git a/src/app/pages/collection/collection.module.ts b/src/app/pages/collection/collection.module.ts index c79cc5b..ce5f5ba 100644 --- a/src/app/pages/collection/collection.module.ts +++ b/src/app/pages/collection/collection.module.ts @@ -47,6 +47,7 @@ import { CollectionPage } from './pages/collection/collection.page'; import { CollectionNFTsPage } from './pages/collection/nfts/nfts.page'; import { UpsertPage } from './pages/upsert/upsert.page'; import { DataService } from './services/data.service'; +import { NzSliderModule } from 'ng-zorro-antd/slider'; @NgModule({ declarations: [CollectionPage, UpsertPage, CollectionAboutComponent, CollectionNFTsPage], @@ -97,6 +98,7 @@ import { DataService } from './services/data.service'; IpfsBadgeModule, IotaInputModule, CollectionMintNetworkModule, + NzSliderModule, ], providers: [DataService, FilterService], }) diff --git a/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts new file mode 100644 index 0000000..c345567 --- /dev/null +++ b/src/app/pages/collection/pages/collection/nfts/collectionNfts.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { Nft, Collection } from '@build-5/interfaces'; +import { CartService } from '@components/cart/services/cart.service'; + +@Injectable({ + providedIn: 'root', +}) +export class CollectionNftStateService { + private listedNftsSubject = new BehaviorSubject([]); + public listedNfts$ = this.listedNftsSubject.asObservable(); + private availableNftsCountSubject = new BehaviorSubject(0); + public availableNftsCount$ = this.availableNftsCountSubject.asObservable(); + + constructor(private cartService: CartService) {} + + public setListedNfts(nfts: Nft[], collection: Collection) { + this.listedNftsSubject.next(nfts); + this.updateAvailableNftsCount(nfts, collection); + } + + private updateAvailableNftsCount(nfts: Nft[], collection: Collection) { + const availableNftsCount = nfts.filter((nft) => + this.cartService.isNftAvailableForSale(nft, collection), + ).length; + this.availableNftsCountSubject.next(availableNftsCount); + } + + public getListedNfts(): Nft[] { + return this.listedNftsSubject.getValue(); + } +} diff --git a/src/app/pages/collection/pages/collection/nfts/nfts.page.html b/src/app/pages/collection/pages/collection/nfts/nfts.page.html index 3ce717a..f49c797 100644 --- a/src/app/pages/collection/pages/collection/nfts/nfts.page.html +++ b/src/app/pages/collection/pages/collection/nfts/nfts.page.html @@ -4,18 +4,39 @@ -
- - - - {{ state.nbHits | number }} records - - - + +
+
+ + +
+ + {{ state.nbHits | number }} records + +
+ + +
+ {{ sweepCount }} + +
+
+
+
+
+
(); + availableNftsCount = 0; + collection: Collection | null = null; constructor( public filter: FilterService, public deviceService: DeviceService, public nftApi: NftApi, + private collectionApi: CollectionApi, public cd: ChangeDetectorRef, public filterStorageService: FilterStorageService, public cacheService: CacheService, public readonly algoliaService: AlgoliaService, + public cartService: CartService, + private notification: NzNotificationService, + private collectionNftStateService: CollectionNftStateService, ) {} public ngOnInit(): void { + if (this.collectionId) { + this.collectionApi + .getCollectionById(this.collectionId) + .pipe(take(1)) + .subscribe({ + next: (collectionData) => { + if (collectionData) { + this.collection = collectionData; + this.collectionNftStateService.setListedNfts(this.originalNfts, this.collection); + } + }, + error: (err) => { + // console.error('Error fetching collection:', err); + this.notification.error($localize`Error occurred while fetching collection.`, ''); + }, + }); + } + + this.collectionNftStateService.availableNftsCount$ + .pipe(takeUntil(this.destroy$)) + .subscribe((count) => { + this.availableNftsCount = count; + }); + // Algolia change detection bug fix setInterval(() => this.cd.markForCheck(), 500); } @@ -91,16 +132,26 @@ export class CollectionNFTsPage implements OnInit, OnChanges { }, 500); } + public captureOriginalHits(hits: any[]) { + if (hits && hits.length > 0 && this.collection) { + this.originalNfts = hits; + this.collectionNftStateService.setListedNfts(hits, this.collection); + } + } + public trackByUid(_index: number, item: any): number { return item.uid; } - public convertAllToSoonaverseModel(algoliaItems: any[]) { - return algoliaItems.map((algolia) => ({ + public convertAllToSoonaverseModel = (algoliaItems: any[]) => { + this.captureOriginalHits(algoliaItems); + + const transformedItems = algoliaItems.map((algolia) => ({ ...algolia, availableFrom: Timestamp.fromMillis(+algolia.availableFrom), })); - } + return transformedItems; + }; public get collapseTypes(): typeof CollapseType { return CollapseType; @@ -109,4 +160,54 @@ export class CollectionNFTsPage implements OnInit, OnChanges { public get algoliaCheckboxFilterTypes(): typeof AlgoliaCheckboxFilterType { return AlgoliaCheckboxFilterType; } + + public sweepToCart(count: number) { + if (!this.collectionId) { + this.notification.error($localize`Collection ID is not available.`, ''); + return; + } + + this.collectionApi + .getCollectionById(this.collectionId) + .pipe( + take(1), + filter((collection): collection is Collection => collection !== undefined), + switchMap((collection: Collection) => { + const listedNfts = this.collectionNftStateService.getListedNfts(); + + const nftsForSale = listedNfts.filter((nft) => + this.cartService.isNftAvailableForSale(nft, collection), + ); + + const nftsToAdd = nftsForSale.slice(0, Math.min(count, 20)).sort((a, b) => { + const priceA = a.availablePrice != null ? a.availablePrice : 0; + const priceB = b.availablePrice != null ? b.availablePrice : 0; + return priceA - priceB; + }); + + nftsToAdd.forEach((nft) => { + const cartItem = { nft, collection, quantity: 1, salePrice: 0 }; + this.cartService.addToCart(cartItem); + }); + + this.notification.success( + $localize`NFTs swept into your cart, open cart to review added items.`, + '', + ); + + return nftsToAdd; + }), + takeUntil(this.destroy$), + ) + .subscribe({ + error: (err) => { + this.notification.error($localize`Error occurred while fetching collection.`, ''); + }, + }); + } + + public ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/src/app/pages/market/pages/collections/collections.page.html b/src/app/pages/market/pages/collections/collections.page.html index a8fa766..1a9b87b 100644 --- a/src/app/pages/market/pages/collections/collections.page.html +++ b/src/app/pages/market/pages/collections/collections.page.html @@ -130,6 +130,7 @@ sortBy: filterStorageService.marketCollectionsFilters$.value.sortBy, range: filterStorageService.marketCollectionsFilters$.value.range, refinementList: { + status: filterStorageService.marketCollectionsFilters$.value.refinementList?.status, category: filterStorageService.marketCollectionsFilters$.value.refinementList?.category, space: filterStorageService.marketCollectionsFilters$.value.refinementList?.space, access: $event @@ -163,6 +164,7 @@ range: filterStorageService.marketCollectionsFilters$.value.range, refinementList: { access: filterStorageService.marketCollectionsFilters$.value.refinementList?.access, + status: filterStorageService.marketCollectionsFilters$.value.refinementList?.status, category: filterStorageService.marketCollectionsFilters$.value.refinementList?.category, space: $event } @@ -193,6 +195,7 @@ refinementList: { access: filterStorageService.marketCollectionsFilters$.value.refinementList?.access, space: filterStorageService.marketCollectionsFilters$.value.refinementList?.space, + status: filterStorageService.marketCollectionsFilters$.value.refinementList?.status, category: $event } } @@ -200,6 +203,36 @@ > + + + +
diff --git a/src/app/pages/market/pages/collections/collections.page.ts b/src/app/pages/market/pages/collections/collections.page.ts index cd60213..e5ee947 100644 --- a/src/app/pages/market/pages/collections/collections.page.ts +++ b/src/app/pages/market/pages/collections/collections.page.ts @@ -34,6 +34,7 @@ export class CollectionsPage implements OnInit { spaceFilterOpen = true; categoryFilterOpen = false; priceFilterOpen = false; + statusFilterOpen = false; constructor( public filter: FilterService, diff --git a/src/app/pages/nft/pages/nft/nft.page.html b/src/app/pages/nft/pages/nft/nft.page.html index 2c6a472..fed7c6f 100644 --- a/src/app/pages/nft/pages/nft/nft.page.html +++ b/src/app/pages/nft/pages/nft/nft.page.html @@ -4,8 +4,21 @@ NFT + NFT + + + {{ getCollectionTypeString(collectionType) }} + + + {{ getCollectionStatusString(collectionMinting) }} + {{ getTitle(data.nft$ | async) }} > -
- + + - + + + + + +
diff --git a/src/app/pages/nft/pages/nft/nft.page.ts b/src/app/pages/nft/pages/nft/nft.page.ts index 78a2e4d..ebab3fe 100644 --- a/src/app/pages/nft/pages/nft/nft.page.ts +++ b/src/app/pages/nft/pages/nft/nft.page.ts @@ -30,6 +30,7 @@ import { HelperService } from '@pages/nft/services/helper.service'; import { Collection, CollectionType, + CollectionStatus, DEFAULT_NETWORK, FILE_SIZES, IPFS_GATEWAY, @@ -47,6 +48,7 @@ import { NzNotificationService } from 'ng-zorro-antd/notification'; import { BehaviorSubject, Subscription, combineLatest, interval, map, skip, take } from 'rxjs'; import { filter } from 'rxjs/operators'; import { DataService } from '../../services/data.service'; +import { CartService } from '@components/cart/services/cart.service'; export enum ListingType { CURRENT_BIDS = 0, @@ -83,10 +85,13 @@ export class NFTPage implements OnInit, OnDestroy { preparing: $localize`Available once minted...`, view: $localize`View`, }; + public currentNft: Nft | null | undefined = null; private subscriptions$: Subscription[] = []; private nftSubscriptions$: Subscription[] = []; private collectionSubscriptions$: Subscription[] = []; private tranSubscriptions$: Subscription[] = []; + public collectionType: CollectionType | null | undefined = null; + public collectionMinting: CollectionStatus | null | undefined = null; constructor( public data: DataService, @@ -109,11 +114,16 @@ export class NFTPage implements OnInit, OnDestroy { private themeService: ThemeService, private seo: SeoService, private notification: NotificationService, + private cartService: CartService, ) { // none } public ngOnInit(): void { + this.data.nft$.subscribe((nft) => { + // console.log('[OnInit] Current NFT:', nft); + this.currentNft = nft; + }); this.deviceService.viewWithSearch$.next(false); this.route.params?.pipe(untilDestroyed(this)).subscribe((obj) => { const id: string | undefined = obj?.[ROUTER_UTILS.config.nft.nft.replace(':', '')]; @@ -268,6 +278,9 @@ export class NFTPage implements OnInit, OnDestroy { this.data.collection$.pipe(skip(1), untilDestroyed(this)).subscribe(async (p) => { if (p) { + this.collectionType = p.type; + // this.collectionMinting = p.status; + // console.log('[nft.page-p] collectionMinting set to: ', p.status) this.collectionSubscriptions$.forEach((s) => { s.unsubscribe(); }); @@ -394,6 +407,24 @@ export class NFTPage implements OnInit, OnDestroy { } } + public getCollectionTypeString(type: CollectionType | null | undefined): string { + if (type === null || type === undefined) { + return 'Unknown'; + } + return CollectionType[type]; + } + + public getCollectionStatusString(status: CollectionStatus | null | undefined): string { + if (status === null || status === undefined) { + return 'Unknown'; + } + console.log( + '[nft.page-getCollectionStatusString] return collection status: ', + CollectionStatus[status], + ); + return CollectionStatus[status]; + } + private listenToNft(id: string): void { this.cancelSubscriptions(); this.data.nftId = id; @@ -523,6 +554,23 @@ export class NFTPage implements OnInit, OnDestroy { } } + public addToCart(nft: Nft): void { + if (nft && this.data.collection$) { + // console.log('[addToCart-this.data.collection$.value', this.data.collection$.value) + // console.log('[addToCart-this.data.nft$.value', this.data.nft$.value) + this.data.collection$.pipe(take(1)).subscribe((collection) => { + if (collection) { + this.cartService.addToCart({ nft, collection, quantity: 1, salePrice: 0 }); + // console.log('Added to cart:', nft, collection); + } else { + // console.error('Collection is undefined or null'); + } + }); + } else { + // console.error('NFT is undefined or null'); + } + } + public generatedNft(nft?: Nft | null): boolean { if (!nft) { return false; diff --git a/src/app/pages/nft/services/helper.service.ts b/src/app/pages/nft/services/helper.service.ts index ae5f98a..f0e5081 100644 --- a/src/app/pages/nft/services/helper.service.ts +++ b/src/app/pages/nft/services/helper.service.ts @@ -124,16 +124,20 @@ export class HelperService { } public getDate(date: any): any { - if (typeof date === 'object' && date?.toDate) { - const d = date.toDate(); - if (!(d instanceof Date) || isNaN(d.valueOf())) { - return undefined; + // console.log(`[getDate] Original input:`, date); + if (typeof date === 'object') { + if (date?.toDate) { + const dateFromObject = date.toDate(); + // console.log(`[getDate] Object with toDate method detected, toDate result:`, dateFromObject); + return dateFromObject; + } else if (date?.seconds) { + const dateFromSeconds = new Date(date.seconds * 1000); // Convert to milliseconds + // console.log(`[getDate] Object with seconds property detected, converted to Date:`, dateFromSeconds); + return dateFromSeconds; } - - return d; - } else { - return date || undefined; } + // console.log(`[getDate] Returning undefined, input could not be parsed as a date.`); + return undefined; } public getCountdownTitle(nft?: Nft | null): string { @@ -168,15 +172,21 @@ export class HelperService { } public isAvailableForSale(nft?: Nft | null, col?: Collection | null): boolean { + // console.log("[NFThelper-isAvailableForSale] function called"); if (!col || !nft?.availableFrom || col?.status === CollectionStatus.MINTING) { + // console.log("[NFT helper.service.ts] isAvailableForSale function returning false. nft name: " + nft?.name + ", col name: " + col?.name) return false; } - return ( + const isAvail = col.approved === true && !!this.getDate(nft.availableFrom) && - dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's') - ); + dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's'); + // console.log("[NFT helper.service.ts] isAvailableForSale function returning " + isAvail + ". nft name: " + nft?.name + ", col name: " + col?.name + ". nft.availableFrom: " + nft.availableFrom.seconds); + // console.log("col.approved: " + (col.approved === true)); + // console.log("!!this.getDate(nft.availableFrom): " + !!this.getDate(nft.availableFrom) + ", this.getDate(nft.availableFrom): " + this.getDate(nft.availableFrom)); + // console.log("dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')" + dayjs(this.getDate(nft.availableFrom)).isSameOrBefore(dayjs(), 's')); + return isAvail; } public canBeSetForSale(nft?: Nft | null): boolean { diff --git a/src/theme/02-components/modal.less b/src/theme/02-components/modal.less index 582a03b..f20460e 100644 --- a/src/theme/02-components/modal.less +++ b/src/theme/02-components/modal.less @@ -2,6 +2,10 @@ @apply bg-backgrounds-tertiary dark:bg-backgrounds-tertiary-dark text-foregrounds-primary dark:text-foregrounds-primary-dark; } +.ant-modal-header { + @apply bg-backgrounds-tertiary dark:bg-backgrounds-tertiary-dark text-foregrounds-primary dark:text-foregrounds-primary-dark; +} + .wen-transaction-select-container { @apply flex flex-col justify-between h-full mt-10 lg:items-center lg:justify-start; } @@ -29,3 +33,8 @@ .wen-modal-tabs { @apply absolute bottom-0 w-full top-56 lg:static; } + +.wen-modal-header { + @apply flex justify-between items-center; + // Additional styling if needed +}