diff --git a/src/components/Activity/Actions/ActionTransfered.tsx b/src/components/Activity/Actions/ActionTransfered.tsx index 4073d3102..5c1cd9ec7 100644 --- a/src/components/Activity/Actions/ActionTransfered.tsx +++ b/src/components/Activity/Actions/ActionTransfered.tsx @@ -13,7 +13,9 @@ export const ActionTransfered: TActionComp = ({ action, verbose }) => ( /> <> transfered{" "} - #{verbose ? action.objkt!.name : action.objkt!.iteration}{" "} + + {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} + {" "} to onChange(!value, event)} /> - + {children} ) diff --git a/src/components/Input/InputMultiIcons.module.scss b/src/components/Input/InputMultiIcons.module.scss new file mode 100644 index 000000000..07bac10b1 --- /dev/null +++ b/src/components/Input/InputMultiIcons.module.scss @@ -0,0 +1,25 @@ +@import "../../styles/Variables.scss"; + +.container { + display: flex; + flex-direction: row; + button { + height: 50px; + margin: 0; + padding: 0 4px; + border: none; + background: transparent; + color: var(--color-border-input); + cursor: pointer; + + &.active { + pointer-events: none; + + color: var(--color-secondary); + } + + &:hover { + color: var(--color-secondary); + } + } +} diff --git a/src/components/Input/InputMultiIcons.tsx b/src/components/Input/InputMultiIcons.tsx new file mode 100644 index 000000000..198f58ad5 --- /dev/null +++ b/src/components/Input/InputMultiIcons.tsx @@ -0,0 +1,36 @@ +import React from "react" +import cs from "classnames" +import style from "./InputMultiIcons.module.scss" + +interface Option { + key?: string + label: any + value: T +} +interface Props { + className: string + options: Option[] + value?: T | null + onChange: (opt: Option) => void +} + +export function InputMultiIcons({ + className, + options, + value = null, + onChange, +}: Props) { + return ( +
+ {options.map((option) => ( + + ))} +
+ ) +} diff --git a/src/components/Input/InputReactiveSearch.module.scss b/src/components/Input/InputReactiveSearch.module.scss index 408714440..34b416994 100644 --- a/src/components/Input/InputReactiveSearch.module.scss +++ b/src/components/Input/InputReactiveSearch.module.scss @@ -1,7 +1,8 @@ .root { display: flex; flex-direction: column; - width: 400px; + max-width: 400px; + width: 100%; position: relative; z-index: 2; } diff --git a/src/components/Navigation/Dropdown.module.scss b/src/components/Navigation/Dropdown.module.scss index cd9bcf0b4..60ae4a351 100644 --- a/src/components/Navigation/Dropdown.module.scss +++ b/src/components/Navigation/Dropdown.module.scss @@ -20,7 +20,6 @@ pointer-events: none; z-index: 100000; visibility: hidden; - & > a { padding-top: 5px !important; padding-bottom: 5px !important; @@ -52,6 +51,10 @@ visibility: visible; } +.menu-top { + bottom: 100% +} + .mobile_static_menu { // avoid undefined error } diff --git a/src/components/Navigation/Dropdown.tsx b/src/components/Navigation/Dropdown.tsx index aed0aa999..51c6321f3 100644 --- a/src/components/Navigation/Dropdown.tsx +++ b/src/components/Navigation/Dropdown.tsx @@ -17,6 +17,7 @@ interface Props { closeOnClick?: boolean mobileMenuAbsolute?: boolean renderComp?: any + direction?: "top" | "bottom" } export function Dropdown({ @@ -28,6 +29,7 @@ export function Dropdown({ children, mobileMenuAbsolute, renderComp, + direction = "bottom", }: PropsWithChildren) { const [opened, setOpened] = useState(false) @@ -90,7 +92,7 @@ export function Dropdown({ > {itemComp} - + {children} diff --git a/src/components/Navigation/DropdownMenu.tsx b/src/components/Navigation/DropdownMenu.tsx index c7d479a43..f4cfc3aa0 100644 --- a/src/components/Navigation/DropdownMenu.tsx +++ b/src/components/Navigation/DropdownMenu.tsx @@ -6,17 +6,20 @@ import cs from "classnames" interface Props { opened: boolean className?: string + direction: "top" | "bottom" } export function DropdownMenu({ opened, className, children, + direction, }: PropsWithChildren) { return (
{children} diff --git a/src/components/TableActions/ModalAddListing.tsx b/src/components/TableActions/ModalAddListing.tsx new file mode 100644 index 000000000..93290bb51 --- /dev/null +++ b/src/components/TableActions/ModalAddListing.tsx @@ -0,0 +1,32 @@ +import React, { memo } from "react" +import { Objkt } from "../../types/entities/Objkt" +import { Modal } from "../Utils/Modal" +import style from "./ModalUserCollection.module.scss" + +import { ListingCreate } from "../../containers/Objkt/ListingCreate" +import { Spacing } from "../Layout/Spacing" + +interface ModalAddListingProps { + objkt: Objkt + onClose: () => void +} + +const _ModalAddListing = ({ objkt, onClose }: ModalAddListingProps) => { + return ( + +
+
Which price would you like to list your gentk for ?
+ +
+ +
+
+
+ ) +} + +export const ModalAddListing = memo(_ModalAddListing) diff --git a/src/components/TableActions/ModalBurnGentk.tsx b/src/components/TableActions/ModalBurnGentk.tsx new file mode 100644 index 000000000..44fec44ac --- /dev/null +++ b/src/components/TableActions/ModalBurnGentk.tsx @@ -0,0 +1,81 @@ +import React, { memo, useCallback, useContext, useState } from "react" +import { Objkt } from "../../types/entities/Objkt" +import { Modal } from "../Utils/Modal" +import style from "./ModalUserCollection.module.scss" +import { Spacing } from "../Layout/Spacing" +import { Button } from "../Button" +import { useContractOperation } from "../../hooks/useContractOperation" +import { ContractFeedback } from "../Feedback/ContractFeedback" +import { UserContext } from "../../containers/UserProvider" +import { + BurnGentkOperation, + TBurnGentkOperationParams, +} from "../../services/contract-operations/BurnGentk" +import { TextWarning } from "../Text/TextWarning" +import { Checkbox } from "../Input/Checkbox" + +interface ModalBurnGentkProps { + objkt: Objkt + onClose: () => void +} + +const _ModalBurnGentk = ({ objkt, onClose }: ModalBurnGentkProps) => { + const [hasAccept, setHasAccept] = useState(false) + const { user } = useContext(UserContext) + const { + state, + loading: contractLoading, + error: contractError, + success, + call, + } = useContractOperation(BurnGentkOperation, { + onSuccess: onClose, + }) + const callContract = useCallback(() => { + if (user) { + call({ + objkt, + fromTzAddress: user.id, + }) + } + }, [call, objkt, user]) + return ( + +
+
Are you sure you want to burn your gentk?
+ + This action is irreversible. + + + I understand that my gentk will be permanently deleted. + + + {(contractLoading || success || contractError) && ( + <> + + + + )} +
+
+ ) +} + +export const ModalBurnGentk = memo(_ModalBurnGentk) diff --git a/src/components/TableActions/ModalCancelListing.tsx b/src/components/TableActions/ModalCancelListing.tsx new file mode 100644 index 000000000..166599419 --- /dev/null +++ b/src/components/TableActions/ModalCancelListing.tsx @@ -0,0 +1,36 @@ +import React, { memo } from "react" +import { Objkt } from "../../types/entities/Objkt" +import { Modal } from "../Utils/Modal" +import style from "./ModalUserCollection.module.scss" + +import { Spacing } from "../Layout/Spacing" +import { ListingCancel } from "../../containers/Objkt/ListingCancel" + +interface ModalCancelListingProps { + objkt: Objkt + onClose: () => void +} + +const _ModalCancelListing = ({ objkt, onClose }: ModalCancelListingProps) => { + return ( + +
+
Are you sure you want to cancel the listing?
+ +
+ +
+
+
+ ) +} + +export const ModalCancelListing = memo(_ModalCancelListing) diff --git a/src/components/TableActions/ModalTransferGentk.tsx b/src/components/TableActions/ModalTransferGentk.tsx new file mode 100644 index 000000000..ecc7a6304 --- /dev/null +++ b/src/components/TableActions/ModalTransferGentk.tsx @@ -0,0 +1,128 @@ +import React, { memo, useCallback, useContext, useMemo, useState } from "react" +import { Objkt } from "../../types/entities/Objkt" +import { Modal } from "../Utils/Modal" +import style from "./ModalUserCollection.module.scss" + +import { InputSearchUser } from "../Input/InputSearchUser" +import { Spacing } from "../Layout/Spacing" +import { User } from "../../types/entities/User" +import { UserBadge } from "../User/UserBadge" +import { isTezosAddress } from "../../utils/strings" +import { Button } from "../Button" +import { useContractOperation } from "../../hooks/useContractOperation" +import { ContractFeedback } from "../Feedback/ContractFeedback" +import { + TransferGentkOperation, + TTransferGentkOperationParams, +} from "../../services/contract-operations/TransferGentk" +import { UserContext } from "../../containers/UserProvider" + +interface ModalTransferGentkProps { + objkt: Objkt + onClose: () => void +} + +const _ModalTransferGentk = ({ objkt, onClose }: ModalTransferGentkProps) => { + const { user } = useContext(UserContext) + const { + state, + loading: contractLoading, + error: contractError, + success, + call, + } = useContractOperation( + TransferGentkOperation, + { + onSuccess: onClose, + } + ) + const [searchValue, setSearchValue] = useState("") + const [fetchedUsers, setFetchedUsers] = useState([]) + const handleSelectUser = useCallback((tzAddress) => { + setSearchValue(tzAddress) + }, []) + + const userSelected = useMemo(() => { + return fetchedUsers.find((user) => user.id === searchValue) + }, [fetchedUsers, searchValue]) + const callContract = useCallback(() => { + if (user) { + call({ + objkt, + fromTzAddress: user.id, + toTzAddress: searchValue, + toUsername: userSelected ? userSelected.name : undefined, + }) + } + }, [call, objkt, searchValue, user, userSelected]) + const hasValidAddress = isTezosAddress(searchValue) + return ( + +
+
+ Please enter a valid tezos address or search by username to transfer + your gentk. +
+ +
+
+ + +
+ {hasValidAddress && ( + <> + +
+ You will transfer your gentk to{" "} + {userSelected ? ( + + ) : ( + {searchValue} + )} + {"."} +
+ + )} + {(contractLoading || success || contractError) && ( + <> + + + + )} +
+
+
+ ) +} + +export const ModalTransferGentk = memo(_ModalTransferGentk) diff --git a/src/components/TableActions/ModalUpdateListing.tsx b/src/components/TableActions/ModalUpdateListing.tsx new file mode 100644 index 000000000..c979a8299 --- /dev/null +++ b/src/components/TableActions/ModalUpdateListing.tsx @@ -0,0 +1,83 @@ +import React, { memo, useCallback, useState } from "react" +import { Objkt } from "../../types/entities/Objkt" +import { Modal } from "../Utils/Modal" +import style from "./ModalUserCollection.module.scss" +import colors from "../../styles/Colors.module.css" + +import { Spacing } from "../Layout/Spacing" +import { ListingUpsert } from "../../containers/Objkt/ListingUpsert" +import { DisplayTezos } from "../Display/DisplayTezos" +import cs from "classnames" +import { calculatePercentageDifference } from "../../utils/math"; + +interface ModalUpdateListingProps { + objkt: Objkt + onClose: () => void +} + +const _ModalUpdateListing = ({ objkt, onClose }: ModalUpdateListingProps) => { + const [newPrice, setNewPrice] = useState(null) + const handleChangePrice = useCallback((updatedPrice) => { + setNewPrice(parseFloat(updatedPrice) * 1000000) + }, []) + + const currentPrice = objkt.activeListing?.price || 0 + const percent = newPrice + ? calculatePercentageDifference(currentPrice, newPrice) + : null + return ( + +
+
Which price would you like to set your listing ?
+ +
+ +
+ {newPrice ? ( +
+ + + + {" => "} + + {percent !== null && ( + 0, + [colors.error]: percent < 0, + [colors.gray]: percent === 0, + })} + > + {" ("} + {percent > 0 ? "+" : ""} + {percent?.toFixed(2)}%) + + )} + +
+ ) : null} +
+
+ ) +} + +export const ModalUpdateListing = memo(_ModalUpdateListing) diff --git a/src/components/TableActions/ModalUserCollection.module.scss b/src/components/TableActions/ModalUserCollection.module.scss new file mode 100644 index 000000000..51658d9bd --- /dev/null +++ b/src/components/TableActions/ModalUserCollection.module.scss @@ -0,0 +1,44 @@ +@import "../../styles/Variables"; +.container { +} +.container_create { + display: flex; + flex-direction: column; + gap: 8px; +} +.container_search { + min-height: 250px; + margin: auto; + .container_search_row { + display: flex; + flex-direction: row; + align-items: flex-start; + } +} +.tz_address { + font-size: var(--font-size-small); + color: var(--color-gray); + background-color: var(--color-gray-vvlight); + overflow-wrap: break-word; + word-break: break-word; +} +.tezos { + color: var(--color-secondary); + font-weight: bold; +} +.btn_transfer {} +.btn_full { + width: 100%; +} + +@media (max-width: $breakpoint-sm) { + .container_search { + .container_search_row { + flex-direction: column; + } + } + .btn_transfer { + margin-top: $spacing-small; + width: 100%; + } +} diff --git a/src/components/TableActions/UserCollectionOwnerActions.module.scss b/src/components/TableActions/UserCollectionOwnerActions.module.scss new file mode 100644 index 000000000..f9a998506 --- /dev/null +++ b/src/components/TableActions/UserCollectionOwnerActions.module.scss @@ -0,0 +1,45 @@ + +.dropdown { + padding: 0; +} +.open_btn { + padding-left: 0; + padding-right: 0; + height: 50px; + width: 50px; + font-size: 18px; + color: var(--color-black); + + &:hover { + border-color: var(--color-secondary); + color: var(--color-secondary); + } +} +.opt { + text-align: left; + padding: 8px 12px; + cursor: pointer; + font-size: var(--font-size-small); + color: var(--color-black); + font-weight: 400; + display: flex; + flex-direction: row; + align-items: center; + &.opt_warning { + font-weight: bold; + color: var(--color-error); + } + i { + font-size: 16px; + margin-right: 10px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + } + &:hover { + color: var(--color-white); + background-color: var(--color-black); + text-decoration: none; + } +} diff --git a/src/components/TableActions/UserCollectionOwnerActions.tsx b/src/components/TableActions/UserCollectionOwnerActions.tsx new file mode 100644 index 000000000..8b06bbe1d --- /dev/null +++ b/src/components/TableActions/UserCollectionOwnerActions.tsx @@ -0,0 +1,128 @@ +import React, { memo, useCallback, useMemo, useState } from "react" +import { Dropdown } from "../Navigation/Dropdown" +import { Objkt } from "../../types/entities/Objkt" +import style from "./UserCollectionOwnerActions.module.scss" +import cs from "classnames" +import { ModalAddListing } from "./ModalAddListing" +import { ModalCancelListing } from "./ModalCancelListing" +import { ModalTransferGentk } from "./ModalTransferGentk" +import { ModalBurnGentk } from "./ModalBurnGentk" +import { ModalUpdateListing } from "./ModalUpdateListing" + +interface DropdownAction { + value: string + label: any + type?: "warning" + modal: React.ElementType +} +const actionsList: Record = { + transfer: { + label: ( + <> + + transfer + + ), + value: "transfer", + modal: ModalTransferGentk, + }, + editListing: { + label: ( + <> + + update listing + + ), + value: "editListing", + modal: ModalUpdateListing, + }, + cancelListing: { + label: ( + <> + + cancel listing + + ), + value: "cancelListing", + modal: ModalCancelListing, + }, + addListing: { + label: ( + <> + + list for trade + + ), + value: "addListing", + modal: ModalAddListing, + }, + burn: { + label: ( + <> + + burn + + ), + value: "burn", + type: "warning", + modal: ModalBurnGentk, + }, +} + +interface UserCollectionOwnerActionsProps { + objkt: Objkt +} + +const _UserCollectionOwnerActions = ({ + objkt, +}: UserCollectionOwnerActionsProps) => { + const [selectedAction, setSelectedAction] = useState(null) + const handleClickAction = useCallback( + (action) => () => setSelectedAction(action), + [] + ) + const handleCloseModal = useCallback(() => { + setSelectedAction(null) + }, []) + const actions = useMemo(() => { + const list = [actionsList.transfer] as DropdownAction[] + if (objkt.activeListing) { + list.push(actionsList.editListing) + list.push(actionsList.cancelListing) + } else { + list.push(actionsList.addListing) + } + list.push(actionsList.burn) + return list + }, [objkt.activeListing]) + const ModalSelectedAction = + selectedAction && actionsList[selectedAction].modal + return ( + <> + } + btnClassName={style.open_btn} + className={style.dropdown} + > + {actions.map((action) => ( +
+ {action.label} +
+ ))} +
+ {ModalSelectedAction && ( + + )} + + ) +} + +export const UserCollectionOwnerActions = memo(_UserCollectionOwnerActions) diff --git a/src/components/Tables/TableUser.module.scss b/src/components/Tables/TableUser.module.scss index fc57291b9..a4574eae2 100644 --- a/src/components/Tables/TableUser.module.scss +++ b/src/components/Tables/TableUser.module.scss @@ -28,6 +28,9 @@ background: var(--color-black); color: var(--color-white); } + &.non_sticky thead { + position: static; + } th { padding: 12px 4px; @@ -39,6 +42,10 @@ } } + &.no_th_padding th:first-child { + padding-left: 4px; + } + td { vertical-align: middle; padding: 4px 4px; @@ -82,6 +89,10 @@ td.td-right { text-align: right; } +.th-select { + width: 50px; +} + .th-gentk { width: 100%; } @@ -224,6 +235,11 @@ td.td-right { display: none; } +td .checkbox, th .checkbox { + margin-bottom: 0; + justify-content: center; +} + @media (max-width: $breakpoint-sm) { .table { padding-top: $spacing; @@ -361,3 +377,4 @@ td.td-right { .flag { margin-left: 8px; } + diff --git a/src/components/Tables/TableUserCollection.tsx b/src/components/Tables/TableUserCollection.tsx new file mode 100644 index 000000000..69783492c --- /dev/null +++ b/src/components/Tables/TableUserCollection.tsx @@ -0,0 +1,192 @@ +import React, { memo, useCallback } from "react" +import style from "./TableUser.module.scss" +import { UserBadge } from "../User/UserBadge" +import { ObjktImageAndName } from "../Objkt/ObjktImageAndName" +import Skeleton from "../Skeleton" +import cs from "classnames" +import { Objkt } from "../../types/entities/Objkt" +import { DisplayTezos } from "../Display/DisplayTezos" +import { Checkbox } from "../Input/Checkbox" +import { UserCollectionOwnerActions } from "../TableActions/UserCollectionOwnerActions" +import { InputTextUnit } from "../Input/InputTextUnit" +import { IconTezos } from "../Icons/IconTezos" + +interface TableUserCollectionProps { + objkts: Objkt[] + isOwner: boolean + loading?: boolean + onSelectRow: (objkt: Objkt) => void + onChangeRow: (objktId: string, rowKey: string, value: any) => void + onSelectAll: () => void + selectedObjkts: Record +} +const _TableUserCollection = ({ + objkts, + loading, + isOwner, + onChangeRow, + onSelectRow, + onSelectAll, + selectedObjkts = {}, +}: TableUserCollectionProps) => { + const handleClickCheckbox = useCallback( + (objkt) => () => { + onSelectRow(objkt) + }, + [onSelectRow] + ) + const handleChangeRowPrice = useCallback( + (objktId, rowKey) => (e: any) => { + onChangeRow(objktId, rowKey, e.target.value * 1000000) + }, + [onChangeRow] + ) + const isAllSelected = + objkts.length > 0 && objkts.length === Object.keys(selectedObjkts).length + return ( + <> + + + + {isOwner && ( + + )} + + + + {isOwner && ( + + )} + + + + {loading || objkts.length > 0 ? ( + objkts.map((objkt) => { + const selectedObjkt = selectedObjkts[objkt.id] + const isSelected = !!selectedObjkt + const priceValue = selectedObjkt?.activeListing?.price + ? selectedObjkt.activeListing.price / 1000000 + : undefined + return ( + + {isOwner && ( + + )} + + + + {isOwner && ( + + )} + + ) + }) + ) : ( + + + + )} + {loading && + [...Array(29)].map((_, idx) => ( + + {isOwner && ( + + )} + + + + {isOwner && ( + + )} + + ))} + +
+ + GentkListed priceAuthor + Actions +
+ + +
+ +
+
+ {isSelected ? ( + } + positionUnit="inside-left" + id="price" + /> + ) : objkt.activeListing?.price ? ( + + ) : ( + <>/ + )} + + + + +
+ No gentks found +
+ + +
+ + +
+
+ + + + + +
+ + ) +} + +export const TableUserCollection = memo(_TableUserCollection) diff --git a/src/components/Utils/Modal.tsx b/src/components/Utils/Modal.tsx index 6be0ed4d6..f9ea7d6e1 100644 --- a/src/components/Utils/Modal.tsx +++ b/src/components/Utils/Modal.tsx @@ -6,12 +6,14 @@ import { useCallback, useContext, useEffect, + useMemo, useRef, } from "react" import effects from "../../styles/Effects.module.scss" import ReactDOM from "react-dom" import { ModalContext } from "../../context/Modal" import { nanoid } from "nanoid" +import useWindowSize, { breakpoints } from "../../hooks/useWindowsSize" type ModalElements = { firstFocusableElement: HTMLElement | null @@ -22,6 +24,7 @@ export interface Props { onClose: () => void index?: number className?: string + width?: string } export function Modal({ title, @@ -29,7 +32,9 @@ export function Modal({ onClose, className, children, + width, }: PropsWithChildren) { + const { width: winWidth } = useWindowSize() const refModalId = useRef(`modal-${nanoid(11)}`) const refModal = useRef(null) const refModalElements = useRef({ @@ -41,35 +46,38 @@ export function Modal({ closeModalId(refModalId.current) onClose() }, [closeModalId, onClose]) - const handleKeyboardNavigation = useCallback((e: KeyboardEvent) => { - if (e.key === "Escape") { - onClose() - return - } - const { firstFocusableElement, lastFocusableElement } = - refModalElements.current - let isTabPressed = e.key === "Tab" || e.keyCode === 9 - if (!isTabPressed) { - return - } - if (e.shiftKey) { - if ( - document.activeElement === firstFocusableElement && - lastFocusableElement - ) { - lastFocusableElement.focus() - e.preventDefault() + const handleKeyboardNavigation = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose() + return } - } else { - if ( - document.activeElement === lastFocusableElement && - firstFocusableElement - ) { - firstFocusableElement.focus() - e.preventDefault() + const { firstFocusableElement, lastFocusableElement } = + refModalElements.current + let isTabPressed = e.key === "Tab" || e.keyCode === 9 + if (!isTabPressed) { + return } - } - }, []) + if (e.shiftKey) { + if ( + document.activeElement === firstFocusableElement && + lastFocusableElement + ) { + lastFocusableElement.focus() + e.preventDefault() + } + } else { + if ( + document.activeElement === lastFocusableElement && + firstFocusableElement + ) { + firstFocusableElement.focus() + e.preventDefault() + } + } + }, + [onClose] + ) useEffect(() => { const modalId = refModalId.current openModalId(modalId) @@ -96,13 +104,19 @@ export function Modal({ document.removeEventListener("keydown", handleKeyboardNavigation) } }, [handleKeyboardNavigation]) + const calculatedWidth = useMemo(() => { + if (winWidth && winWidth < breakpoints.sm) { + return undefined + } + return width + }, [width, winWidth]) return ReactDOM.createPortal( <>
void } -export function ListingCancel({ listing, objkt }: Props) { +export function ListingCancel({ listing, objkt, onSuccess }: Props) { const { state, loading: contractLoading, @@ -23,7 +24,10 @@ export function ListingCancel({ listing, objkt }: Props) { success, call, } = useContractOperation( - ListingCancelOperation + ListingCancelOperation, + { + onSuccess, + } ) const callContract = () => { diff --git a/src/containers/Objkt/ListingCreate.tsx b/src/containers/Objkt/ListingCreate.tsx index b185ff0dc..089cd3d81 100644 --- a/src/containers/Objkt/ListingCreate.tsx +++ b/src/containers/Objkt/ListingCreate.tsx @@ -16,10 +16,16 @@ import { IconTezos } from "../../components/Icons/IconTezos" interface Props { objkt: Objkt + defaultOpen?: boolean + onSuccess?: () => void } -export function ListingCreate({ objkt }: Props) { - const [opened, setOpened] = useState(false) +export function ListingCreate({ + objkt, + onSuccess, + defaultOpen = false, +}: Props) { + const [opened, setOpened] = useState(defaultOpen) const [price, setPrice] = useState("") const { @@ -28,7 +34,9 @@ export function ListingCreate({ objkt }: Props) { error: contractError, success, call, - } = useContractOperation(ListingOperation) + } = useContractOperation(ListingOperation, { + onSuccess, + }) const callContract = () => { const mutez = Math.floor(parseFloat(price) * 1000000) @@ -42,7 +50,10 @@ export function ListingCreate({ objkt }: Props) { } } - const floor = useMemo(() => objkt.issuer?.marketStats?.floor || 0, []) + const floor = useMemo( + () => objkt.issuer?.marketStats?.floor || 0, + [objkt.issuer?.marketStats?.floor] + ) const showWarningListingTooLow = useMemo(() => { const mutez = parseFloat(price) * 1000000 const isFloorOver100tz = floor > 100 * 1000000 @@ -63,8 +74,8 @@ export function ListingCreate({ objkt }: Props) { <> {showWarningListingTooLow && ( - Your listing is priced way under
- the floor () + Your listing is priced way under the floor ( + )
)}
diff --git a/src/containers/Objkt/ListingUpsert.tsx b/src/containers/Objkt/ListingUpsert.tsx new file mode 100644 index 000000000..929f2c24d --- /dev/null +++ b/src/containers/Objkt/ListingUpsert.tsx @@ -0,0 +1,119 @@ +import style from "./MarketplaceActions.module.scss" +import cs from "classnames" +import { useCallback, useMemo, useState } from "react" +import { Button } from "../../components/Button" +import { InputTextUnit } from "../../components/Input/InputTextUnit" +import { Objkt } from "../../types/entities/Objkt" +import { ContractFeedback } from "../../components/Feedback/ContractFeedback" +import { useContractOperation } from "../../hooks/useContractOperation" +import { TextWarning } from "../../components/Text/TextWarning" +import { DisplayTezos } from "../../components/Display/DisplayTezos" +import { IconTezos } from "../../components/Icons/IconTezos" +import { + ListingUpsertOperation, + TListingUpsertOperationParams, +} from "../../services/contract-operations/ListingUpsert" + +interface Props { + objkt: Objkt + defaultPrice?: number + onSuccess?: () => void + onChangePrice?: (price: string) => void +} + +export function ListingUpsert({ + objkt, + defaultPrice, + onSuccess, + onChangePrice, +}: Props) { + const [price, setPrice] = useState( + defaultPrice ? defaultPrice.toString() : "" + ) + + const { + state, + loading: contractLoading, + error: contractError, + success, + call, + } = useContractOperation( + ListingUpsertOperation, + { + onSuccess, + } + ) + + const callContract = () => { + const mutez = Math.floor(parseFloat(price) * 1000000) + if (isNaN(mutez)) { + alert("Invalid price") + } else { + call({ + token: objkt, + price: mutez, + }) + } + } + + const handleChangePrice = useCallback( + (evt) => { + const newPrice = evt.target.value + setPrice(newPrice) + if (onChangePrice) { + onChangePrice(newPrice) + } + }, + [onChangePrice] + ) + const floor = useMemo( + () => objkt.issuer?.marketStats?.floor || 0, + [objkt.issuer?.marketStats?.floor] + ) + const showWarningListingTooLow = useMemo(() => { + const mutez = parseFloat(price) * 1000000 + const isFloorOver100tz = floor > 100 * 1000000 + const isPriceUnderHalfFloor = price !== undefined && mutez <= floor * 0.5 + return isFloorOver100tz && isPriceUnderHalfFloor + }, [floor, price]) + return ( + <> + + {showWarningListingTooLow && ( + + Your listing is priced way under the floor ( + ) + + )} +
+ } + positionUnit="inside-left" + type="number" + sizeX="fill" + value={price} + onChange={handleChangePrice} + min={0} + step={0.0000001} + className={style.input} + /> + +
+ + ) +} diff --git a/src/containers/User/Collection/DisplayToggle.module.scss b/src/containers/User/Collection/DisplayToggle.module.scss new file mode 100644 index 000000000..0ead9dfa8 --- /dev/null +++ b/src/containers/User/Collection/DisplayToggle.module.scss @@ -0,0 +1,4 @@ +.display { + padding-right: 8px; + font-size: 25px; +} diff --git a/src/containers/User/Collection/DisplayToggle.tsx b/src/containers/User/Collection/DisplayToggle.tsx new file mode 100644 index 000000000..8b79bcdea --- /dev/null +++ b/src/containers/User/Collection/DisplayToggle.tsx @@ -0,0 +1,51 @@ +import React, { memo, useCallback } from "react" +import { InputMultiIcons } from "../../../components/Input/InputMultiIcons" +import style from "./DisplayToggle.module.scss" + +const options = [ + { + label: ( + + ), + value: "grid", + }, + { + label: ( + + ), + value: "list", + }, +] + +type DisplayMode = "list" | "grid" +interface DisplayToggleProps { + onChange: (displayMode: DisplayMode) => void + value: DisplayMode +} + +const _DisplayToggle = ({ onChange, value }: DisplayToggleProps) => { + const handleChange = useCallback( + (opt) => { + onChange(opt.value) + }, + [onChange] + ) + return ( + + ) +} + +export const DisplayToggle = memo(_DisplayToggle) diff --git a/src/containers/User/Collection/Gentks.tsx b/src/containers/User/Collection/Gentks.tsx index f0593aea3..357f4ecc8 100644 --- a/src/containers/User/Collection/Gentks.tsx +++ b/src/containers/User/Collection/Gentks.tsx @@ -6,7 +6,7 @@ import cs from "classnames" import { IUserCollectionFilters, User } from "../../../types/entities/User" import { CardsExplorer } from "../../../components/Exploration/CardsExplorer" import { SearchHeader } from "../../../components/Search/SearchHeader" -import { IOptions, Select } from "../../../components/Input/Select" +import { Select } from "../../../components/Input/Select" import { SearchInputControlled } from "../../../components/Input/SearchInputControlled" import { FiltersPanel } from "../../../components/Exploration/FiltersPanel" import { UserCollectionFilters } from "../UserCollectionFilters" @@ -19,57 +19,34 @@ import { InfiniteScrollTrigger } from "../../../components/Utils/InfiniteScrollT import { CardsContainer } from "../../../components/Card/CardsContainer" import { ObjktCard } from "../../../components/Card/ObjktCard" import { CardsLoading } from "../../../components/Card/CardsLoading" -import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react" import { useQuery } from "@apollo/client" import { Qu_userObjkts } from "../../../queries/user" import { CardSizeSelect } from "../../../components/Input/CardSizeSelect" import { GentksActions } from "./GentksActions" import { useQueryParamSort } from "hooks/useQueryParamSort" +import { sortOptionsUserGentk } from "../../../utils/sort" +import useFilters from "../../../hooks/useFilters" +import { ListingFilters } from "../../../types/entities/Listing" +import { getTagsFromFiltersObject } from "../../../utils/filters" +import { DisplayToggle } from "./DisplayToggle" +import { GentksList } from "./GentksList" +import { UserContext } from "../../UserProvider" const ITEMS_PER_PAGE = 40 -const generalSortOptions: IOptions[] = [ - { - label: "recently minted", - value: "createdAt-desc", - }, - { - label: "oldest minted", - value: "createdAt-asc", - }, - { - label: "recently bought", - value: "collectedAt-desc", - }, - { - label: "rarity (rarest first)", - value: "rarity-asc", - }, - { - label: "rarity (rarest last)", - value: "rarity-desc", - }, -] - -const searchSortOptions: IOptions[] = [ - { - label: "search relevance", - value: "relevance-desc", - }, - ...generalSortOptions, -] - -function sortValueToSortVariable(val: string) { - if (val === "pertinence") return {} - const split = val.split("-") - return { - [split[0]]: split[1].toUpperCase(), - } -} - interface Props { user: User } export function UserCollectionGentks({ user }: Props) { + const { user: userLogged } = useContext(UserContext) + const [displayMode, setDisplayMode] = useState<"list" | "grid">("list") const [hasNothingToFetch, setHasNothingToFetch] = useState(false) const { @@ -79,35 +56,44 @@ export function UserCollectionGentks({ user }: Props) { setSortValue, setSearchSortOptions, restoreSort, - } = useQueryParamSort(generalSortOptions) + } = useQueryParamSort(sortOptionsUserGentk) // filters - const [filters, setFilters] = useState({}) + const { filters, setFilters, addFilter, removeFilter } = + useFilters({ + onAdd: (filter, updatedFilters) => { + if (filter === "searchQuery_eq") { + setSearchSortOptions() + } + }, + onRemove: (filter, updatedFilters) => { + if (filter === "searchQuery_eq") { + restoreSort() + } + }, + }) // reference to an element at the top to scroll back const topMarkerRef = useRef(null) - const { data, loading, fetchMore, refetch } = useQuery<{ user: User }>( - Qu_userObjkts, - { - notifyOnNetworkStatusChange: true, - variables: { - id: user.id, - skip: 0, - take: ITEMS_PER_PAGE, - filters, - sort: sortVariable, - }, - onCompleted: (newData) => { - if ( - !newData?.user?.objkts?.length || - newData.user.objkts.length < ITEMS_PER_PAGE - ) { - setHasNothingToFetch(true) - } - }, - } - ) + const { data, loading, fetchMore } = useQuery<{ user: User }>(Qu_userObjkts, { + notifyOnNetworkStatusChange: true, + variables: { + id: user.id, + skip: 0, + take: ITEMS_PER_PAGE, + filters, + sort: sortVariable, + }, + onCompleted: (newData) => { + if ( + !newData?.user?.objkts?.length || + newData.user.objkts.length < ITEMS_PER_PAGE + ) { + setHasNothingToFetch(true) + } + }, + }) // safe access to gentks const objkts = data?.user?.objkts || null @@ -134,77 +120,36 @@ export function UserCollectionGentks({ user }: Props) { if (window.scrollY > top + 10) { window.scrollTo(0, top) } - - refetch?.({ - skip: 0, - take: ITEMS_PER_PAGE, - sort: sortVariable, - filters, - }) + setHasNothingToFetch(false) }, [sortVariable, filters]) - const addFilter = (filter: string, value: any) => { - setFilters({ - ...filters, - [filter]: value, - }) - } - - const removeFilter = (filter: string) => { - addFilter(filter, undefined) - // if the filter is search string, we reset the sort to what ti was - if (filter === "searchQuery_eq" && sortValue === "relevance-desc") { - restoreSort() - } - } - - // build the list of filters - const filterTags = useMemo(() => { - const tags: ExploreTagDef[] = [] - for (const key in filters) { - let value: string | null = null - // @ts-ignore - if (filters[key] !== undefined) { - switch (key) { - case "assigned_eq": - //@ts-ignore - value = `metadata assigned: ${filters[key] ? "yes" : "no"} tez` - break - case "authorVerified_eq": - //@ts-ignore - value = `artist: ${filters[key] ? "verified" : "un-verified"}` - break - case "mintProgress_eq": - //@ts-ignore - value = `mint progress: ${filters[key]?.toLowerCase()}` - break - case "searchQuery_eq": - //@ts-ignore - value = `search: ${filters[key]}` - break - case "author_in": - //@ts-ignore - value = `artists: (${filters[key].length})` - break - case "issuer_in": - //@ts-ignore - value = `generators: (${filters[key].length})` - break - case "activeListing_exist": - value = `listing: ${filters[key] ? "listed" : "not listed"}` - break - } - if (value) { - tags.push({ - value, - onClear: () => removeFilter(key), - }) - } + const handleSearch = useCallback( + (value) => { + if (value) { + addFilter("searchQuery_eq", value) + } else { + removeFilter("searchQuery_eq") } - } - return tags - }, [filters]) + }, + [addFilter, removeFilter] + ) + const handleClearTags = useCallback(() => { + setFilters({}) + restoreSort() + }, [restoreSort, setFilters]) + const filterTags = useMemo( + () => + getTagsFromFiltersObject( + filters, + ({ label, key }) => ({ + value: label, + onClear: () => removeFilter(key), + }) + ), + [filters, removeFilter] + ) + const isUserLogged = userLogged?.id === user.id return ( {({ @@ -231,6 +176,7 @@ export function UserCollectionGentks({ user }: Props) { [styleCardsExplorer["hide-sort"]]: !isSearchMinimized, })} > +