diff --git a/.env b/.env index bf3eb81c6..d0163a7f0 100644 --- a/.env +++ b/.env @@ -1,6 +1,7 @@ NEXT_PUBLIC_API_ROOT = https://api.fxhash.xyz/graphql NEXT_PUBLIC_API_FILE_ROOT = https://file-api.fxhash.xyz NEXT_PUBLIC_API_EXTRACT = https://extract.fxhash.xyz +NEXT_PUBLIC_API_EVENTS_ROOT = https://events.fxhash.xyz NEXT_PUBLIC_IPFS_GATEWAY = https://gateway.fxhash.xyz NEXT_PUBLIC_IPFS_GATEWAY_SAFE = https://gateway.fxhash2.xyz NEXT_PUBLIC_TZKT_API = https://api.tzkt.io/v1/ @@ -19,13 +20,16 @@ NEXT_PUBLIC_TZ_CT_ADDRESS_ISSUER = KT1BJC12dG17CVvPKJ1VYaNnaT5mzfnUTwXv NEXT_PUBLIC_TZ_CT_ADDRESS_USERREGISTER = KT1Ezht4PDKZri7aVppVGT4Jkw39sesaFnww NEXT_PUBLIC_TZ_CT_ADDRESS_TOK_MODERATION = KT18tPu7uXy9PJ97i3qCLsr7an4X6sQ5qxU7 NEXT_PUBLIC_TZ_CT_ADDRESS_USER_MODERATION = KT1Wn2kkKmdbyLWBiLXWCkE7fKj1LsLKar2A +NEXT_PUBLIC_TZ_CT_ADDRESS_ARTICLE_MODERATION = KT1A36z7nG4zPDbhjyrzhYf9SCn5ipPZeRMQ NEXT_PUBLIC_TZ_CT_ADDRESS_TEAM_MODERATION = KT1FvGQcPxzuJkJsdWFQiGkueSNT5mqpFDrf NEXT_PUBLIC_TZ_CT_ADDRESS_CYCLES = KT1BgD9SPfysnMz3vkfm6ZEaGFKCVcE5ay91 NEXT_PUBLIC_TZ_CT_ADDRESS_GENTK_V1 = KT1KEa8z6vWXDJrVqtMrAeDVzsvxat3kHaCE NEXT_PUBLIC_TZ_CT_ADDRESS_GENTK_V2 = KT1U6EHmNxJTkvaWJ4ThczG4FSDaHC21ssvi +NEXT_PUBLIC_TZ_CT_ADDRESS_ARTICLES = KT1GtbuswcNMGhHF2TSuH1Yfaqn16do8Qtva NEXT_PUBLIC_TZ_CT_ADDRESS_MARKETPLACE_V1 = KT1Xo5B7PNBAeynZPmca4bRh6LQow4og1Zb9 NEXT_PUBLIC_TZ_CT_ADDRESS_MARKETPLACE_V2 = KT1GbyoDi7H1sfXmimXpptZJuCdHMh66WS9u +NEXT_PUBLIC_TZ_CT_ADDRESS_MARKETPLACE_V3 = KT1M1NyU9X4usEimt2f3kDaijZnDMNBu42Ja NEXT_PUBLIC_TZ_CT_ADDRESS_COLLAB_FACTORY = KT1JrUPSCt1r2MB2J7Lk2KwiWSYr3Mr414ck NEXT_PUBLIC_TZ_CT_ADDRESS_ALLOWED_MINT_ISSUER = KT1Djz5ix2yEGmV7PMq3GYq17TvMMkd1anT2 diff --git a/.eslintrc.json b/.eslintrc.json index bffb357a7..ab41625b3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,12 @@ { - "extends": "next/core-web-vitals" + "plugins": [ + "prettier" + ], + "extends": [ + "prettier", + "next/core-web-vitals" + ], + "rules": { + "prettier/prettier": "error" + } } diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..0a379d53a --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": false, + "singleQuote": false +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index 0123273a6..3eb8271ec 100644 --- a/next.config.js +++ b/next.config.js @@ -9,6 +9,24 @@ if (!process.env.NEXT_PUBLIC_IPFS_GATEWAY || !process.env.NEXT_PUBLIC_IPFS_GATEW const urlGateway = new URL(process.env.NEXT_PUBLIC_IPFS_GATEWAY); const urlGatewaySafe = new URL(process.env.NEXT_PUBLIC_IPFS_GATEWAY_SAFE); +// the main common security headers +const baseSecurityHeaders = [ + { + key: "X-Frame-Options", + value: "SAMEORIGIN" + }, + // Isolates the browsing context exclusively to same-origin documents. + // Cross-origin documents are not loaded in the same browsing context. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy + { + key: 'Cross-Origin-Opener-Policy', + value: 'same-origin' + }, +] + +const articlesAllowedDomains = "https://*.spotify.com/ https://spotify.com https://*.youtube.com/ https://youtube.com https://*.twitter.com/ https://twitter.com" + + /** @type {import('next').NextConfig} */ module.exports = withBundleAnalyzer({ reactStrictMode: true, @@ -19,6 +37,16 @@ module.exports = withBundleAnalyzer({ async headers() { return [ + { + source: "/:path*", + headers: [ + { + key: "Content-Security-Policy", + value: `frame-ancestors 'self'; frame-src ${process.env.NEXT_PUBLIC_IPFS_GATEWAY_SAFE} ${articlesAllowedDomains} 'self';` + }, + ...baseSecurityHeaders, + ] + }, { source: "/sandbox/worker.js", headers: [ @@ -27,6 +55,19 @@ module.exports = withBundleAnalyzer({ value: "/" } ] + }, + { + source: "/sandbox/preview.html", + headers: [ + { + key: "Content-Security-Policy", + value: "" + }, + { + key: "Cross-Origin-Embedder-Policy", + value: "require-corp" + }, + ] } ] }, diff --git a/package.json b/package.json index 2497f17c8..80685687b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "lint:fix": "next lint --fix" }, "dependencies": { "@apollo/client": "^3.4.16", @@ -24,6 +25,7 @@ "classnames": "^2.3.1", "date-fns": "^2.25.0", "date-fns-tz": "^1.1.6", + "detect-incognito": "^1.0.0", "echarts": "^5.2.2", "file-type": "^16.5.3", "formik": "^2.2.9", @@ -50,7 +52,7 @@ "react-router-dom": "^5.3.0", "react-simple-code-editor": "^0.11.2", "react-textarea-autosize": "^8.3.4", - "rehype-format": "^4.0.0", + "rehype-format": "^4.0.1", "rehype-highlight": "^5.0.0", "rehype-katex": "^6.0.2", "rehype-prism": "^2.1.3", @@ -86,6 +88,10 @@ "@types/react-router-dom": "^5.3.1", "eslint": "8.0.0", "eslint-config-next": "11.1.2", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-prettier": "^4.0.0", + "eslint-plugin-react": "^7.31.8", + "prettier": "2.6.2", "typescript": "4.4.3" } } diff --git a/public/images/logos/fxhash-black.svg b/public/images/logos/fxhash-black.svg new file mode 100644 index 000000000..51fd20548 --- /dev/null +++ b/public/images/logos/fxhash-black.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/articles/guide-mint-generative-token.md b/src/articles/guide-mint-generative-token.md index a620e8c3f..965f197d7 100644 --- a/src/articles/guide-mint-generative-token.md +++ b/src/articles/guide-mint-generative-token.md @@ -100,7 +100,7 @@ This snippet serves 2 purposes: The code snippet exposes 4 variables: -- `fxhash`: a random 51 characters base 58 encoded string (designed to have the same signature has a Tezos transaction hash). When someone mints a unique Token from a Generative Token, the transaction hash is hardcoded in place of the code that generates a random one. +- `fxhash`: a random 51 characters base 58 encoded string (designed to have the same signature as a Tezos transaction hash). When someone mints a unique Token from a Generative Token, the transaction hash is hardcoded in place of the code that generates a random one. - `fxrand()`: a PRNG function that generates deterministic PRN between 0 and 1. **Simply use it instead of Math.random()**. - `fxpreview()`: a function you can call whenever the code is ready to be captured - `isFxpreview`: a boolean, true when the code is executed to take the capture, false otherwise diff --git a/src/components/Activity/Action.module.scss b/src/components/Activity/Action.module.scss index 0668b50c2..c4fc20a34 100644 --- a/src/components/Activity/Action.module.scss +++ b/src/components/Activity/Action.module.scss @@ -102,6 +102,12 @@ z-index: 1; } +.link { + position: relative; + z-index: 1; + color: currentColor; +} + .align { display: flex; align-items: center; @@ -116,4 +122,4 @@ right: 0; display: block; z-index: 1; -} \ No newline at end of file +} diff --git a/src/components/Activity/Action.tsx b/src/components/Activity/Action.tsx index 7616fe3c1..3a1bbcf78 100644 --- a/src/components/Activity/Action.tsx +++ b/src/components/Activity/Action.tsx @@ -1,28 +1,17 @@ import style from "./Action.module.scss" -import effects from "../../styles/Effects.module.scss" import colors from "../../styles/Colors.module.css" import cs from "classnames" -import { Action as ActionType, TokenActionType } from "../../types/entities/Action" -import { UserBadge } from "../User/UserBadge" -import { FunctionComponent, useMemo, PropsWithChildren } from "react" -import { format, formatDistance, formatRelative, subDays } from 'date-fns' -import { displayMutez, displayRoyalties } from "../../utils/units" +import { Action as ActionType } from "../../types/entities/Action" +import { useMemo, PropsWithChildren } from "react" import Link from "next/link" -import { DisplayTezos } from "../Display/DisplayTezos" - +import { TActionLinkFn } from "./Actions/Action" +import { DateDistance } from "../Utils/Date/DateDistance" +import { ActionDefinitions } from "./ActionDefinitions" interface Props { action: ActionType verbose: boolean } - -type TActionComp = FunctionComponent - -export const DateDistance = ({ timestamptz, append = false }: { timestamptz: string, append?: boolean }) => { - const dist = useMemo(() => formatDistance(new Date(timestamptz), new Date(), { addSuffix: true, }), []) - return { dist } -} - export const ActionReference = ({ action }: { action: ActionType }) => { return ( { target="_blank" rel="noreferrer" > - - + + ) } -const IconTransfer = () => ( - -) - -const IconSend = () => ( - -) - -const IconRefresh = () => ( - -) - -const IconBurn = () => ( - -) - -const IconCreate = () => ( - -) - -const IconCreateGentok = () => ( - -) - -const IconSignGentok = () => ( - -) - -const IconCheck = () => ( - -) - -const IconCancel = () => ( - -) - -/** - * MINT OPERATIONS - */ - -const ActionMinted: FunctionComponent = ({ action, verbose }) => ( - <> - - - published { action.token!.name } - - -) - -const ActionMintedFrom: FunctionComponent = ({ action, verbose }) => ( - <> - - - minted - {verbose ? ( - {action.objkt!.name} - ):( - #{action.objkt!.iteration} - )} - - -) - -const ActionSigned: FunctionComponent = ({ action, verbose }) => ( - <> - - metadata was signed by fxhash - - -) - -const ActionTransfered: FunctionComponent = ({ action, verbose }) => ( - <> - - - transfered #{verbose ? action.objkt!.name : action.objkt!.iteration} to - - - -) - -/** - * TOKEN UPDATES - */ - - const ActionUpdatePrice: TActionComp = ({ action }) => ( - <> - - - updated price: - - - - - - {" -> "} - - - - - -) - -const ActionUpdateState: FunctionComponent = ({ action }) => { - const changes = action.metadata.changes - return ( - <> - - - updated state: - - - {changes.enabled !== undefined && ( - - - {changes.enabled ? "enabled" : "disabled"} - - {changes.royalties !== undefined ? ", " : ""} - - )} - {changes.royalties !== undefined && ( - - - {displayRoyalties(changes.royalties)} - royalties - - )} - - - ) -} - -const ActionBurnSupply: FunctionComponent = ({ action }) => { - const metadata = action.metadata - return ( - <> - - - burnt some supply: - - - {metadata.from}{" -> "}{metadata.to} - - - ) -} - -/** - * LISTINGS - */ - -const ActionListing: TActionComp = ({ action, verbose }) => ( - <> - - - listed {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} for - - - - - -) - -const ActionListingAccepted: TActionComp = ({ action, verbose }) => ( - <> - - - bought {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} from - - - - for - - - - - -) - -const ActionListingCancelled: TActionComp = ({ action }) => ( - <> - - - cancelled their listing on #{action.objkt!.iteration} - - -) - -/** - * OFFERS (on single gentks) - */ - -const ActionOffer: FunctionComponent = ({ action, verbose }) => ( - <> - - offered - - - - for {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} - -) - -const ActionOfferAccepted: FunctionComponent = ({ action, verbose }) => ( - <> - - offer of - - - - on {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} - accepted by - - -) - -const ActionOfferCancelled: FunctionComponent = ({ action, verbose }) => ( - <> - - cancelled - - - - offer for {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} - -) - - -const ActionCompleted: FunctionComponent = ({ action }) => ( - <> - completed - - Generative Token fully minted - -) - -const ActionTODO: TActionComp = ({ action }) => null - -const ActionMapComponent: Record> = { - MINTED: ActionMinted, - MINTED_FROM: ActionMintedFrom, - TRANSFERED: ActionTransfered, - GENTK_SIGNED: ActionSigned, - - LISTING_V1: ActionListing, - LISTING_V1_ACCEPTED: ActionListingAccepted, - LISTING_V1_CANCELLED: ActionListingCancelled, - LISTING_V2: ActionListing, - LISTING_V2_ACCEPTED: ActionListingAccepted, - LISTING_V2_CANCELLED: ActionListingCancelled, - - OFFER: ActionOffer, - OFFER_ACCEPTED: ActionOfferAccepted, - OFFER_CANCELLED: ActionOfferCancelled, - - UPDATE_PRICING: ActionUpdatePrice, - UPDATE_STATE: ActionUpdateState, - - BURN_SUPPLY: ActionBurnSupply, - COMPLETED: ActionCompleted, - - // TODO - NONE: ActionTODO, - COLLECTION_OFFER: ActionTODO, - COLLECTION_OFFER_CANCELLED: ActionTODO, - COLLECTION_OFFER_ACCEPTED: ActionTODO, - AUCTION: ActionTODO, - AUCTION_BID: ActionTODO, - AUCTION_CANCELLED: ActionTODO, - AUCTION_FULFILLED: ActionTODO, -} - -const actionMapLink: Record string|null> = { - MINTED: (action: ActionType) => `/generative/${action.token?.id}`, - MINTED_FROM: (action: ActionType) => `/gentk/${action.objkt?.id}`, - GENTK_SIGNED: (action: ActionType) => null, - TRANSFERED: (action: ActionType) => `/gentk/${action.objkt?.id}`, - LISTING_V1: (action: ActionType) => `/gentk/${action.objkt?.id}`, - LISTING_V1_ACCEPTED: (action: ActionType) => `/gentk/${action.objkt?.id}`, - LISTING_V1_CANCELLED: (action: ActionType) => `/gentk/${action.objkt?.id}`, - LISTING_V2: (action: ActionType) => `/gentk/${action.objkt?.id}`, - LISTING_V2_ACCEPTED: (action: ActionType) => `/gentk/${action.objkt?.id}`, - LISTING_V2_CANCELLED: (action: ActionType) => `/gentk/${action.objkt?.id}`, - OFFER: (action: ActionType) => `/gentk/${action.objkt?.id}`, - OFFER_ACCEPTED: (action: ActionType) => `/gentk/${action.objkt?.id}`, - OFFER_CANCELLED: (action: ActionType) => `/gentk/${action.objkt?.id}`, - UPDATE_STATE: (action: ActionType) => `/generative/${action.token?.id}`, - UPDATE_PRICING: (action: ActionType) => `/generative/${action.token?.id}`, - BURN_SUPPLY: (action: ActionType) => `/gentk/${action.token?.id}`, - COMPLETED: (action: ActionType) => `/generative/${action.token?.id}`, - // TODO - NONE: (action: ActionType) => null, - COLLECTION_OFFER: (action: ActionType) => null, - COLLECTION_OFFER_CANCELLED: (action: ActionType) => null, - COLLECTION_OFFER_ACCEPTED: (action: ActionType) => null, - AUCTION: (action: ActionType) => null, - AUCTION_BID: (action: ActionType) => null, - AUCTION_CANCELLED: (action: ActionType) => null, - AUCTION_FULFILLED: (action: ActionType) => null, -} - -const ActionMapIcon: Record = { - MINTED: IconCreateGentok, - MINTED_FROM: IconCreate, - GENTK_SIGNED: IconSignGentok, - COMPLETED: IconCheck, - TRANSFERED: IconTransfer, - LISTING_V1: IconSend, - LISTING_V1_ACCEPTED: IconTransfer, - LISTING_V1_CANCELLED: IconCancel, - LISTING_V2: IconSend, - LISTING_V2_ACCEPTED: IconTransfer, - LISTING_V2_CANCELLED: IconCancel, - UPDATE_PRICING: IconRefresh, - UPDATE_STATE: IconRefresh, - BURN_SUPPLY: IconBurn, - OFFER: IconSend, - OFFER_ACCEPTED: IconTransfer, - OFFER_CANCELLED: IconCancel, - // TODO - NONE: IconCancel, - COLLECTION_OFFER: IconCancel, - COLLECTION_OFFER_CANCELLED: IconCancel, - COLLECTION_OFFER_ACCEPTED: IconCancel, - AUCTION: IconCancel, - AUCTION_BID: IconCancel, - AUCTION_CANCELLED: IconCancel, - AUCTION_FULFILLED: IconCancel, -} - // some actions may have a link to a page - which requires some tricky logic - -function LinkWrapper({ action, children }: PropsWithChildren<{ action: ActionType }>) { - const link = actionMapLink[action.type] && actionMapLink[action.type](action) - return link - ? ( - - ):( -
{ children }
- ) +type ILinkWrapperProps = PropsWithChildren<{ + action: ActionType + linkFn?: TActionLinkFn | null +}> +function LinkWrapper({ action, linkFn, children }: ILinkWrapperProps) { + const link = useMemo(() => linkFn?.(action) || null, [action, linkFn]) + + return link ? ( +
+ ) : ( +
{children}
+ ) } export function Action({ action, verbose }: Props) { - const ActionComponent = ActionMapComponent[action.type] - const ActionIcon = ActionMapIcon[action.type] + const def = useMemo(() => ActionDefinitions[action.type], [action.type]) - if (!ActionComponent) { + if (!def?.render) { return
todo {action.type}
} return ( - +
- +
- + {def.render({ action, verbose })}
- +
) diff --git a/src/components/Activity/ActionDefinitions.ts b/src/components/Activity/ActionDefinitions.ts new file mode 100644 index 000000000..c19067901 --- /dev/null +++ b/src/components/Activity/ActionDefinitions.ts @@ -0,0 +1,225 @@ +import { + Action as ActionType, + TokenActionType, +} from "../../types/entities/Action" +import { getArticleUrl } from "../../utils/entities/articles" +import { ActionDefinition, TActionLinkFn } from "./Actions/Action" +import { ActionMinted } from "./Actions/ActionMinted" +import { ActionMintedFrom } from "./Actions/ActionMintedFrom" +import { ActionSigned } from "./Actions/ActionSigned" +import { ActionCompleted } from "./Actions/ActionMintCompleted" +import { ActionTransfered } from "./Actions/ActionTransfered" +import { ActionTODO } from "./Actions/ActionTODO" +import { ActionListing } from "./Actions/ActionListing" +import { ActionListingAccepted } from "./Actions/ActionListingAccepted" +import { ActionListingCancelled } from "./Actions/ActionListingCancelled" +import { ActionUpdatePrice } from "./Actions/ActionUpdatePrice" +import { ActionUpdateState } from "./Actions/ActionUpdateState" +import { ActionBurnSupply } from "./Actions/ActionBurnSupply" +import { ActionOffer } from "./Actions/ActionOffer" +import { ActionOfferAccepted } from "./Actions/ActionOfferAccepted" +import { ActionArticleMinted } from "./Actions/ActionArticleMinted" +import { ActionArticleEditionsTransfered } from "./Actions/ActionArticleTransfered" +import { ActionArticleUpdated } from "./Actions/ActionArticleUpdated" +import { ActionArticleLocked } from "./Actions/ActionArticleLocked" +import { ActionListingV3 } from "./Actions/ActionListingV3" +import { ActionListingAcceptedV3 } from "./Actions/ActionListingAcceptedV3" +import { ActionListingCancelledV3 } from "./Actions/ActionListingCancelledV3" +import { getObjktUrl } from "../../utils/objkt" + +const ActionLinks = { + gentk: (action: ActionType) => `/gentk/${action.objkt?.id}`, + token: (action: ActionType) => `/generative/${action.token?.id}`, + article: (action: ActionType) => getArticleUrl(action.article!), + gentkOrArticle: (action: ActionType) => + action.article ? getArticleUrl(action.article) : getObjktUrl(action.objkt!), +} as const + +const ActionTodoDefinition: ActionDefinition = { + icon: "", + iconColor: "error", + render: ActionTODO, + predecescence: 0, + link: null, +} + +export const ActionDefinitions: Record = { + MINTED: { + icon: "fa-solid fa-user-robot", + iconColor: "success", + render: ActionMinted, + predecescence: 0, + link: ActionLinks.token, + }, + MINTED_FROM: { + icon: "fa-solid fa-sparkles", + iconColor: "success", + render: ActionMintedFrom, + predecescence: 0, + link: ActionLinks.gentk, + }, + GENTK_SIGNED: { + icon: "fa-solid fa-signature", + iconColor: "success", + render: ActionSigned, + predecescence: 0, + link: null, + }, + COMPLETED: { + icon: "fas fa-check-circle", + iconColor: "success", + render: ActionCompleted, + predecescence: 0, + link: ActionLinks.token, + }, + TRANSFERED: { + icon: "fa-regular fa-arrow-right-arrow-left", + iconColor: "success", + render: ActionTransfered, + predecescence: 0, + link: ActionLinks.gentk, + }, + LISTING_V1: { + icon: "fa-regular fa-arrow-turn-up", + iconColor: "success", + render: ActionListing, + predecescence: 0, + link: ActionLinks.gentk, + }, + LISTING_V1_ACCEPTED: { + icon: "fa-regular fa-arrow-right-arrow-left", + iconColor: "success", + render: ActionListingAccepted, + predecescence: 0, + link: ActionLinks.gentk, + }, + LISTING_V1_CANCELLED: { + icon: "fa-solid fa-xmark", + iconColor: "error", + render: ActionListingCancelled, + predecescence: 0, + link: ActionLinks.gentk, + }, + LISTING_V2: { + icon: "fa-regular fa-arrow-turn-up", + iconColor: "success", + render: ActionListing, + predecescence: 0, + link: ActionLinks.gentk, + }, + LISTING_V2_ACCEPTED: { + icon: "fa-regular fa-arrow-right-arrow-left", + iconColor: "success", + render: ActionListingAccepted, + predecescence: 0, + link: ActionLinks.gentk, + }, + LISTING_V2_CANCELLED: { + icon: "fa-solid fa-xmark", + iconColor: "error", + render: ActionListingCancelled, + predecescence: 0, + link: ActionLinks.gentk, + }, + LISTING_V3: { + icon: "fa-regular fa-arrow-turn-up", + iconColor: "success", + render: ActionListingV3, + predecescence: 0, + link: ActionLinks.gentkOrArticle, + }, + LISTING_V3_ACCEPTED: { + icon: "fa-regular fa-arrow-right-arrow-left", + iconColor: "success", + render: ActionListingAcceptedV3, + predecescence: 0, + link: ActionLinks.gentkOrArticle, + }, + LISTING_V3_CANCELLED: { + icon: "fa-solid fa-xmark", + iconColor: "error", + render: ActionListingCancelledV3, + predecescence: 0, + link: ActionLinks.gentkOrArticle, + }, + UPDATE_PRICING: { + icon: "fa-solid fa-arrow-rotate-right", + iconColor: "warning", + render: ActionUpdatePrice, + predecescence: 0, + link: ActionLinks.token, + }, + UPDATE_STATE: { + icon: "fa-solid fa-arrow-rotate-right", + iconColor: "warning", + render: ActionUpdateState, + predecescence: 0, + link: ActionLinks.token, + }, + BURN_SUPPLY: { + icon: "fa-solid fa-fire", + iconColor: "warning", + render: ActionBurnSupply, + predecescence: 0, + link: ActionLinks.token, + }, + OFFER: { + icon: "fa-regular fa-arrow-turn-up", + iconColor: "success", + render: ActionOffer, + predecescence: 0, + link: ActionLinks.gentk, + }, + OFFER_ACCEPTED: { + icon: "fa-regular fa-arrow-right-arrow-left", + iconColor: "success", + render: ActionOfferAccepted, + predecescence: 0, + link: ActionLinks.gentk, + }, + OFFER_CANCELLED: { + icon: "fa-solid fa-xmark", + iconColor: "error", + render: ActionListingCancelled, + predecescence: 0, + link: ActionLinks.gentk, + }, + ARTICLE_MINTED: { + icon: "fa-sharp fa-solid fa-memo", + iconColor: "success", + render: ActionArticleMinted, + predecescence: 0, + link: ActionLinks.article, + }, + ARTICLE_EDITIONS_TRANSFERED: { + icon: "fa-regular fa-arrow-right-arrow-left", + iconColor: "success", + render: ActionArticleEditionsTransfered, + predecescence: 0, + link: ActionLinks.article, + }, + ARTICLE_METADATA_UPDATED: { + icon: "fa-sharp fa-solid fa-pen-to-square", + iconColor: "warning", + render: ActionArticleUpdated, + predecescence: 0, + link: ActionLinks.article, + }, + ARTICLE_METADATA_LOCKED: { + icon: "fa-solid fa-lock", + iconColor: "warning", + render: ActionArticleLocked, + predecescence: 0, + link: ActionLinks.article, + }, + + // TODO + NONE: ActionTodoDefinition, + COLLECTION_OFFER: ActionTodoDefinition, + COLLECTION_OFFER_CANCELLED: ActionTodoDefinition, + COLLECTION_OFFER_ACCEPTED: ActionTodoDefinition, + AUCTION: ActionTodoDefinition, + AUCTION_BID: ActionTodoDefinition, + AUCTION_CANCELLED: ActionTodoDefinition, + AUCTION_FULFILLED: ActionTodoDefinition, +} diff --git a/src/components/Activity/Actions/Action.ts b/src/components/Activity/Actions/Action.ts new file mode 100644 index 000000000..f736e7be0 --- /dev/null +++ b/src/components/Activity/Actions/Action.ts @@ -0,0 +1,20 @@ +import { FunctionComponent } from "react" +import { TColor } from "../../../types/Colors" +import { Action } from "../../../types/entities/Action" + +interface Props { + action: Action + verbose: boolean +} + +export type TActionComp = FunctionComponent + +export type TActionLinkFn = (action: Action) => string | null + +export interface ActionDefinition { + icon: string + iconColor: TColor + render: TActionComp + predecescence: number + link: TActionLinkFn | null +} diff --git a/src/components/Activity/Actions/ActionArticleLocked.tsx b/src/components/Activity/Actions/ActionArticleLocked.tsx new file mode 100644 index 000000000..34ce483af --- /dev/null +++ b/src/components/Activity/Actions/ActionArticleLocked.tsx @@ -0,0 +1,18 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" + +export const ActionArticleLocked: TActionComp = ({ action, verbose }) => ( + <> + + + locked article {action.article!.title} + + +) diff --git a/src/components/Activity/Actions/ActionArticleMinted.tsx b/src/components/Activity/Actions/ActionArticleMinted.tsx new file mode 100644 index 000000000..1a990ecd7 --- /dev/null +++ b/src/components/Activity/Actions/ActionArticleMinted.tsx @@ -0,0 +1,18 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" + +export const ActionArticleMinted: TActionComp = ({ action, verbose }) => ( + <> + + + minted article {action.article!.title} + + +) diff --git a/src/components/Activity/Actions/ActionArticleTransfered.tsx b/src/components/Activity/Actions/ActionArticleTransfered.tsx new file mode 100644 index 000000000..0f2a9a38d --- /dev/null +++ b/src/components/Activity/Actions/ActionArticleTransfered.tsx @@ -0,0 +1,33 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" + +export const ActionArticleEditionsTransfered: TActionComp = ({ + action, + verbose, +}) => ( + <> + + + transfered {action.numericValue} editions{" "} + {verbose && ( + <> + of {action.article!.title} + + )}{" "} + to + + + +) diff --git a/src/components/Activity/Actions/ActionArticleUpdated.tsx b/src/components/Activity/Actions/ActionArticleUpdated.tsx new file mode 100644 index 000000000..8eb017b2d --- /dev/null +++ b/src/components/Activity/Actions/ActionArticleUpdated.tsx @@ -0,0 +1,55 @@ +import { ipfsGatewayUrl } from "../../../services/Ipfs" +import { getNumberWithOrdinal } from "../../../utils/math" +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { NFTArticleRevision } from "../../../types/entities/Article" + +interface ArticleRevisionLinkProps { + revision: NFTArticleRevision +} + +function ArticleRevisionLink({ revision }: ArticleRevisionLinkProps) { + return ( +
+ + {revision.iteration === 0 + ? "initial mint" + : getNumberWithOrdinal(revision.iteration) + " revision"} + + + ) +} + +export const ActionArticleUpdated: TActionComp = ({ action, verbose }) => { + const { from, to } = action.metadata! + const revisionFrom = action.article!.revisions!.find( + (r) => r.metadataUri === from + ) + const revisionTo = action.article!.revisions!.find( + (r) => r.metadataUri === to + ) + + return ( + <> + + + updated article {` `} + + {` `} to {` `} + + + + ) +} diff --git a/src/components/Activity/Actions/ActionBurnSupply.tsx b/src/components/Activity/Actions/ActionBurnSupply.tsx new file mode 100644 index 000000000..f9b5dfa04 --- /dev/null +++ b/src/components/Activity/Actions/ActionBurnSupply.tsx @@ -0,0 +1,25 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import colors from "../../../styles/Colors.module.css" + +export const ActionBurnSupply: TActionComp = ({ action }) => { + const metadata = action.metadata + return ( + <> + + burnt some supply: + + {metadata.from} + {" -> "} + {metadata.to} + + + ) +} diff --git a/src/components/Activity/Actions/ActionListing.tsx b/src/components/Activity/Actions/ActionListing.tsx new file mode 100644 index 000000000..4ff1e26bc --- /dev/null +++ b/src/components/Activity/Actions/ActionListing.tsx @@ -0,0 +1,30 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +export const ActionListing: TActionComp = ({ action, verbose }) => ( + <> + + + listed{" "} + + {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} + {" "} + for + + + + + +) diff --git a/src/components/Activity/Actions/ActionListingAccepted.tsx b/src/components/Activity/Actions/ActionListingAccepted.tsx new file mode 100644 index 000000000..8b7f5c39e --- /dev/null +++ b/src/components/Activity/Actions/ActionListingAccepted.tsx @@ -0,0 +1,39 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +export const ActionListingAccepted: TActionComp = ({ action, verbose }) => ( + <> + + + bought{" "} + + {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} + {" "} + from + + + + for + + + + + +) diff --git a/src/components/Activity/Actions/ActionListingAcceptedV3.tsx b/src/components/Activity/Actions/ActionListingAcceptedV3.tsx new file mode 100644 index 000000000..9563f6ed5 --- /dev/null +++ b/src/components/Activity/Actions/ActionListingAcceptedV3.tsx @@ -0,0 +1,81 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +const ActionListingArticleAccepted: TActionComp = ({ action, verbose }) => ( + <> + + + bought {action.metadata.amountCollected} editions{" "} + {verbose && ( + <> + of {action.article!.title} + + )}{" "} + from + + + + for + + + + each + + +) + +const ActionListingObjktAccepted: TActionComp = ({ action, verbose }) => ( + <> + + + bought{" "} + + {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} + {" "} + from + + + + for + + + + + +) + +export const ActionListingAcceptedV3: TActionComp = (props) => { + // todo: when add support for gentks, add a filter upfront to pick right comp. + return ActionListingArticleAccepted(props) +} diff --git a/src/components/Activity/Actions/ActionListingCancelled.tsx b/src/components/Activity/Actions/ActionListingCancelled.tsx new file mode 100644 index 000000000..37d93e0e2 --- /dev/null +++ b/src/components/Activity/Actions/ActionListingCancelled.tsx @@ -0,0 +1,18 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" + +export const ActionListingCancelled: TActionComp = ({ action }) => ( + <> + + + cancelled their listing on #{action.objkt!.iteration} + + +) diff --git a/src/components/Activity/Actions/ActionListingCancelledV3.tsx b/src/components/Activity/Actions/ActionListingCancelledV3.tsx new file mode 100644 index 000000000..a6871ae16 --- /dev/null +++ b/src/components/Activity/Actions/ActionListingCancelledV3.tsx @@ -0,0 +1,38 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +const ActionListingArticleCancelledV3: TActionComp = ({ action, verbose }) => ( + <> + + + cancelled listing of {action.metadata.amount} editions{" "} + {verbose && ( + <> + on {action.article!.title} + + )}{" "} + of{" "} + + + + each + + +) + +export const ActionListingCancelledV3: TActionComp = (props) => { + // todo: when add support for gentks, add a filter upfront to pick right comp. + return ActionListingArticleCancelledV3(props) +} diff --git a/src/components/Activity/Actions/ActionListingV3.tsx b/src/components/Activity/Actions/ActionListingV3.tsx new file mode 100644 index 000000000..c01b1aa99 --- /dev/null +++ b/src/components/Activity/Actions/ActionListingV3.tsx @@ -0,0 +1,38 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +const ActionListingArticle: TActionComp = ({ action, verbose }) => ( + <> + + + listed {action.metadata.amount} editions{" "} + {verbose && ( + <> + of {action.article!.title} + + )}{" "} + for + + + + + each + +) + +export const ActionListingV3: TActionComp = (props) => { + // todo: when add support for gentks, add a filter upfront to pick right comp. + return ActionListingArticle(props) +} diff --git a/src/components/Activity/Actions/ActionMintCompleted.tsx b/src/components/Activity/Actions/ActionMintCompleted.tsx new file mode 100644 index 000000000..3b4f362bf --- /dev/null +++ b/src/components/Activity/Actions/ActionMintCompleted.tsx @@ -0,0 +1,13 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import colors from "../../../styles/Colors.module.css" + +export const ActionCompleted: TActionComp = ({ action }) => ( + <> + completed + + Generative Token fully minted + +) diff --git a/src/components/Activity/Actions/ActionMinted.tsx b/src/components/Activity/Actions/ActionMinted.tsx new file mode 100644 index 000000000..f206ce857 --- /dev/null +++ b/src/components/Activity/Actions/ActionMinted.tsx @@ -0,0 +1,18 @@ +import { UserBadge } from "../../User/UserBadge" +import { TActionComp } from "./Action" +import style from "../Action.module.scss" +import cs from "classnames" + +export const ActionMinted: TActionComp = ({ action, verbose }) => ( + <> + + + published {action.token!.name} + + +) diff --git a/src/components/Activity/Actions/ActionMintedFrom.tsx b/src/components/Activity/Actions/ActionMintedFrom.tsx new file mode 100644 index 000000000..6b437c476 --- /dev/null +++ b/src/components/Activity/Actions/ActionMintedFrom.tsx @@ -0,0 +1,23 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" + +export const ActionMintedFrom: TActionComp = ({ action, verbose }) => ( + <> + + + minted + {verbose ? ( + {action.objkt!.name} + ) : ( + #{action.objkt!.iteration} + )} + + +) diff --git a/src/components/Activity/Actions/ActionOffer.tsx b/src/components/Activity/Actions/ActionOffer.tsx new file mode 100644 index 000000000..62e0378ce --- /dev/null +++ b/src/components/Activity/Actions/ActionOffer.tsx @@ -0,0 +1,30 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +export const ActionOffer: TActionComp = ({ action, verbose }) => ( + <> + + offered + + + + + for{" "} + + {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} + + + +) diff --git a/src/components/Activity/Actions/ActionOfferAccepted.tsx b/src/components/Activity/Actions/ActionOfferAccepted.tsx new file mode 100644 index 000000000..5f42de65d --- /dev/null +++ b/src/components/Activity/Actions/ActionOfferAccepted.tsx @@ -0,0 +1,37 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +export const ActionOfferAccepted: TActionComp = ({ action, verbose }) => ( + <> + + offer of + + + + + on{" "} + + {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} + + + accepted by + + +) diff --git a/src/components/Activity/Actions/ActionOfferCancelled.tsx b/src/components/Activity/Actions/ActionOfferCancelled.tsx new file mode 100644 index 000000000..70b5de0f6 --- /dev/null +++ b/src/components/Activity/Actions/ActionOfferCancelled.tsx @@ -0,0 +1,30 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +export const ActionOfferCancelled: TActionComp = ({ action, verbose }) => ( + <> + + cancelled + + + + + offer for{" "} + + {verbose ? action.objkt!.name : `#${action.objkt!.iteration}`} + + + +) diff --git a/src/components/Activity/Actions/ActionSigned.tsx b/src/components/Activity/Actions/ActionSigned.tsx new file mode 100644 index 000000000..1b7b5df07 --- /dev/null +++ b/src/components/Activity/Actions/ActionSigned.tsx @@ -0,0 +1,7 @@ +import { TActionComp } from "./Action" + +export const ActionSigned: TActionComp = ({ action, verbose }) => ( + <> + metadata was signed by fxhash + +) diff --git a/src/components/Activity/Actions/ActionTODO.tsx b/src/components/Activity/Actions/ActionTODO.tsx new file mode 100644 index 000000000..dc622dc4f --- /dev/null +++ b/src/components/Activity/Actions/ActionTODO.tsx @@ -0,0 +1,4 @@ +import { TActionComp } from "./Action" + +// used as a fallback if action is undefined +export const ActionTODO: TActionComp = ({ action }) => null diff --git a/src/components/Activity/Actions/ActionTransfered.tsx b/src/components/Activity/Actions/ActionTransfered.tsx new file mode 100644 index 000000000..8f3bc53ee --- /dev/null +++ b/src/components/Activity/Actions/ActionTransfered.tsx @@ -0,0 +1,26 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" + +export const ActionTransfered: TActionComp = ({ action, verbose }) => ( + <> + + + transfered{" "} + #{verbose ? action.objkt!.name : action.objkt!.iteration}{" "} + to + + + +) diff --git a/src/components/Activity/Actions/ActionUpdatePrice.tsx b/src/components/Activity/Actions/ActionUpdatePrice.tsx new file mode 100644 index 000000000..7a63cc847 --- /dev/null +++ b/src/components/Activity/Actions/ActionUpdatePrice.tsx @@ -0,0 +1,34 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import { DisplayTezos } from "../../Display/DisplayTezos" + +export const ActionUpdatePrice: TActionComp = ({ action }) => ( + <> + + updated price: + + + + + {" -> "} + + + + + +) diff --git a/src/components/Activity/Actions/ActionUpdateState.tsx b/src/components/Activity/Actions/ActionUpdateState.tsx new file mode 100644 index 000000000..eb1794e48 --- /dev/null +++ b/src/components/Activity/Actions/ActionUpdateState.tsx @@ -0,0 +1,41 @@ +import style from "../Action.module.scss" +import cs from "classnames" +import { TActionComp } from "./Action" +import { UserBadge } from "../../User/UserBadge" +import colors from "../../../styles/Colors.module.css" +import { displayRoyalties } from "../../../utils/units" + +export const ActionUpdateState: TActionComp = ({ action }) => { + const changes = action.metadata.changes + return ( + <> + + updated state: + + {changes.enabled !== undefined && ( + + + {changes.enabled ? "enabled" : "disabled"} + + {changes.royalties !== undefined ? ", " : ""} + + )} + {changes.royalties !== undefined && ( + + + {displayRoyalties(changes.royalties)} + {" "} + royalties + + )} + + + ) +} diff --git a/src/components/Activity/Activity.tsx b/src/components/Activity/Activity.tsx index e1d2ceeda..abcb2cf1a 100644 --- a/src/components/Activity/Activity.tsx +++ b/src/components/Activity/Activity.tsx @@ -1,37 +1,46 @@ import style from "./Activity.module.scss" import cs from "classnames" -import { Action as ActionType, TokenActionType } from "../../types/entities/Action" +import { + Action as ActionType, + TokenActionType, +} from "../../types/entities/Action" import { Action } from "./Action" import { useMemo } from "react" import Skeleton from "../Skeleton" - const ActionsPredecescence: Record = { - NONE : 0, - UPDATE_STATE : 0, - UPDATE_PRICING : 0, - BURN_SUPPLY : 0, - MINTED : 0, - MINTED_FROM : 1, - GENTK_SIGNED : 1, - COMPLETED : 20, - TRANSFERED : 1, - LISTING_V1 : 4, - LISTING_V1_CANCELLED : 4, - LISTING_V1_ACCEPTED : 4, - LISTING_V2 : 4, - LISTING_V2_CANCELLED : 4, - LISTING_V2_ACCEPTED : 4, - OFFER : 4, - OFFER_CANCELLED : 4, - OFFER_ACCEPTED : 4, - COLLECTION_OFFER : 4, - COLLECTION_OFFER_CANCELLED : 4, - COLLECTION_OFFER_ACCEPTED : 4, - AUCTION : 4, - AUCTION_BID : 4, - AUCTION_CANCELLED : 4, - AUCTION_FULFILLED : 4, + NONE: 0, + UPDATE_STATE: 0, + UPDATE_PRICING: 0, + BURN_SUPPLY: 0, + MINTED: 0, + MINTED_FROM: 1, + GENTK_SIGNED: 1, + COMPLETED: 20, + TRANSFERED: 1, + LISTING_V1: 4, + LISTING_V1_CANCELLED: 4, + LISTING_V1_ACCEPTED: 4, + LISTING_V2: 4, + LISTING_V2_CANCELLED: 4, + LISTING_V2_ACCEPTED: 4, + LISTING_V3: 4, + LISTING_V3_CANCELLED: 4, + LISTING_V3_ACCEPTED: 4, + OFFER: 4, + OFFER_CANCELLED: 4, + OFFER_ACCEPTED: 4, + COLLECTION_OFFER: 4, + COLLECTION_OFFER_CANCELLED: 4, + COLLECTION_OFFER_ACCEPTED: 4, + AUCTION: 4, + AUCTION_BID: 4, + AUCTION_CANCELLED: 4, + AUCTION_FULFILLED: 4, + ARTICLE_MINTED: 0, + ARTICLE_EDITIONS_TRANSFERED: 1, + ARTICLE_METADATA_UPDATED: 1, + ARTICLE_METADATA_LOCKED: 20, } // group actions by timestamp, sort by type within group, then rebuild array @@ -39,14 +48,15 @@ function sortActions(actions: ActionType[]): ActionType[] { // batch actions by timestamp const batches: Record = {} for (const action of actions) { - if (!batches[action.createdAt]) - batches[action.createdAt] = [] + if (!batches[action.createdAt]) batches[action.createdAt] = [] batches[action.createdAt].push(action) } // sort each batch, and rebuild the output array at the same time let ret: ActionType[] = [] for (const k in batches) { - batches[k] = batches[k].sort((a, b) => ActionsPredecescence[b.type] - ActionsPredecescence[a.type]) + batches[k] = batches[k].sort( + (a, b) => ActionsPredecescence[b.type] - ActionsPredecescence[a.type] + ) ret = [...ret, ...batches[k]] } return ret @@ -63,7 +73,7 @@ export function Activity({ actions, className, verbose = false, - loading = false + loading = false, }: Props) { const sortedActions = useMemo(() => sortActions(actions), [actions]) @@ -71,21 +81,16 @@ export function Activity({
{sortedActions?.length > 0 || loading ? ( <> - {sortedActions?.length > 0 && ( - sortedActions.map(action => ( + {sortedActions?.length > 0 && + sortedActions.map((action) => ( - )) - )} - {loading && ( + ))} + {loading && [...Array(20)].map((_, idx) => ( - - )) - )} + + ))} - ):( + ) : ( No activity yet )}
diff --git a/src/components/Article/Actions/ArticleListEditions.module.scss b/src/components/Article/Actions/ArticleListEditions.module.scss new file mode 100644 index 000000000..531bb8adb --- /dev/null +++ b/src/components/Article/Actions/ArticleListEditions.module.scss @@ -0,0 +1,5 @@ +.modal_content { + font-size: var(--font-size-regular); + width: min(600px, 100vw); + padding: 8px; +} \ No newline at end of file diff --git a/src/components/Article/Actions/ArticleListEditions.tsx b/src/components/Article/Actions/ArticleListEditions.tsx new file mode 100644 index 000000000..9fa8b4045 --- /dev/null +++ b/src/components/Article/Actions/ArticleListEditions.tsx @@ -0,0 +1,142 @@ +import style from "./ArticleListEditions.module.scss" +import cs from "classnames" +import { Form, Formik } from "formik" +import { useCallback, useMemo, useState } from "react" +import { NFTArticle } from "../../../types/entities/Article" +import { Ledger } from "../../../types/entities/Ledger" +import { Button } from "../../Button" +import { Field } from "../../Form/Field" +import { SliderWithText } from "../../Input/SliderWithText" +import { SliderWithTextInput } from "../../Input/SliderWithTextInput" +import { Modal } from "../../Utils/Modal" +import { InputTextUnit } from "../../Input/InputTextUnit" +import { Submit } from "../../Form/Submit" +import { Spacing } from "../../Layout/Spacing" +import * as Yup from "yup" +import { YupPrice } from "../../../utils/yup/price" +import { useContractOperation } from "../../../hooks/useContractOperation" +import { ListingV3Operation } from "../../../services/contract-operations/ListingV3" +import { ContractFeedback } from "../../Feedback/ContractFeedback" + +interface Props { + ledger: Ledger + article: NFTArticle +} +export function ArticleListEditions({ ledger, article }: Props) { + const [showModal, setShowModal] = useState(false) + + const validation = useMemo(() => { + return Yup.object({ + amount: Yup.number() + .typeError("Valid number plz") + .required("Required") + .min(1, "Min 1") + .max(ledger.amount, `Max ${ledger.amount}`), + price: YupPrice, + }) + }, [ledger]) + + const { call, loading, success, error, state } = + useContractOperation(ListingV3Operation) + + const list = useCallback( + (amount: string, price: string) => { + call({ + article: article, + owner: ledger.owner, + amount: amount, + price: ((price as any) * 1000000).toString(), + }) + }, + [article, ledger] + ) + + return ( + <> + + + {showModal && ( + setShowModal(false)} + > + { + list(values.amount, values.price) + }} + validationSchema={validation} + > + {({ values, setFieldValue, errors }) => ( +
+ + + setFieldValue("amount", val)} + min={1} + max={ledger.amount} + step={1} + textTransform={(v) => v as any} + unit="editions" + /> + + + + + setFieldValue("price", evt.target.value)} + // onBlur={onBlur} + // error={!!errors?.price} + /> + + + + + + + + + + + )} +
+
+ )} + + ) +} diff --git a/src/components/Article/Article.tsx b/src/components/Article/Article.tsx index 79723a10a..fed97c21f 100644 --- a/src/components/Article/Article.tsx +++ b/src/components/Article/Article.tsx @@ -2,17 +2,9 @@ import style from "./ArticleContent.module.scss" import cs from "classnames" import { PropsWithChildren } from "react" - interface Props { className?: string } -export function Article({ - className, - children, -}: PropsWithChildren) { - return ( -
- {children} -
- ) -} \ No newline at end of file +export function Article({ className, children }: PropsWithChildren) { + return
{children}
+} diff --git a/src/components/Article/ArticleContent.tsx b/src/components/Article/ArticleContent.tsx index 24568778c..661a57993 100644 --- a/src/components/Article/ArticleContent.tsx +++ b/src/components/Article/ArticleContent.tsx @@ -1,7 +1,6 @@ import style from "./ArticleContent.module.scss" import cs from "classnames" - interface Props { content: string } @@ -11,8 +10,8 @@ export function ArticleContent({ content }: Props) {
) -} \ No newline at end of file +} diff --git a/src/components/Artwork/ArtworkFrame.tsx b/src/components/Artwork/ArtworkFrame.tsx index 7f292b6e5..8931bf8df 100644 --- a/src/components/Artwork/ArtworkFrame.tsx +++ b/src/components/Artwork/ArtworkFrame.tsx @@ -14,4 +14,4 @@ export function ArtworkFrame({ {children} ) -} \ No newline at end of file +} diff --git a/src/components/Artwork/Generative.tsx b/src/components/Artwork/Generative.tsx index f72c529ce..b0705095d 100644 --- a/src/components/Artwork/Generative.tsx +++ b/src/components/Artwork/Generative.tsx @@ -1,3 +1,3 @@ export function ArtworkGenerative() { return null -} \ No newline at end of file +} diff --git a/src/components/Artwork/MintProgress.tsx b/src/components/Artwork/MintProgress.tsx index 491555842..cf2e1e426 100644 --- a/src/components/Artwork/MintProgress.tsx +++ b/src/components/Artwork/MintProgress.tsx @@ -8,7 +8,6 @@ import { UserContext } from "../../containers/UserProvider" import { reserveEligibleAmount } from "../../utils/generative-token" import { User } from "../../types/entities/User" - interface Props { token: GenerativeToken showReserve?: boolean @@ -21,54 +20,42 @@ export function MintProgress({ const settings = useContext(SettingsContext) const { user } = useContext(UserContext) - const { - balance, - supply, - originalSupply, - } = token + const { balance, supply, originalSupply } = token // number of iterations minted const minted = supply - balance const complete = balance === 0 - const [progress, burntProgress, reserveSize, reserveProgress] = - useMemo<[number, number, number, number]>( - () => { - const visibleSupply = (settings.displayBurntCard ? originalSupply : supply) - const progress = minted / visibleSupply - const burnt = originalSupply - supply - const burntProgress = settings.displayBurntCard - ? (burnt / originalSupply) - : 0 - const reserveSize = token.reserves - ? token.reserves.reduce((a, b) => a + b.amount, 0) - : 0 - const reserveProgress = Math.min( - 1, reserveSize / visibleSupply - ) - return [ - progress, - burntProgress, - reserveSize, - reserveProgress, - ] - } - , [settings]) + const [progress, burntProgress, reserveSize, reserveProgress] = useMemo< + [number, number, number, number] + >(() => { + const visibleSupply = settings.displayBurntCard ? originalSupply : supply + const progress = minted / visibleSupply + const burnt = originalSupply - supply + const burntProgress = settings.displayBurntCard ? burnt / originalSupply : 0 + const reserveSize = token.reserves + ? token.reserves.reduce((a, b) => a + b.amount, 0) + : 0 + const reserveProgress = Math.min(1, reserveSize / visibleSupply) + return [progress, burntProgress, reserveSize, reserveProgress] + }, [settings]) // compute how many editions in reserve the user is eligible for - const eligibleFor = useMemo(() => - user - ? reserveEligibleAmount(user as User, token) - : 0 - , [user, token]) + const eligibleFor = useMemo( + () => (user ? reserveEligibleAmount(user as User, token) : 0), + [user, token] + ) return (
- + - {minted}/{supply} minted + {minted}/{supply}{" "} + minted {complete && } {children} @@ -77,20 +64,20 @@ export function MintProgress({
@@ -104,4 +91,4 @@ export function MintProgress({ )}
) -} \ No newline at end of file +} diff --git a/src/components/Artwork/Preview.tsx b/src/components/Artwork/Preview.tsx index 9c04f2eca..a1bc35c0e 100644 --- a/src/components/Artwork/Preview.tsx +++ b/src/components/Artwork/Preview.tsx @@ -1,14 +1,14 @@ -import { ipfsGatewayUrl } from '../../services/Ipfs' -import { useLazyImage } from '../../utils/hookts' -import { Loader } from '../Utils/Loader' -import style from './Artwork.module.scss' -import { ArtworkFrame } from './ArtworkFrame' +import { ipfsGatewayUrl } from "../../services/Ipfs" +import { useLazyImage } from "../../utils/hookts" +import { Loader } from "../Utils/Loader" +import style from "./Artwork.module.scss" +import { ArtworkFrame } from "./ArtworkFrame" interface Props { ipfsUri?: string url?: string alt?: string - loading?: string|boolean + loading?: string | boolean } export function ArtworkPreview({ @@ -26,4 +26,4 @@ export function ArtworkPreview({ {loading && !loaded && } ) -} \ No newline at end of file +} diff --git a/src/components/Artwork/PreviewIframe.tsx b/src/components/Artwork/PreviewIframe.tsx index 48ace3972..c4b1fc74f 100644 --- a/src/components/Artwork/PreviewIframe.tsx +++ b/src/components/Artwork/PreviewIframe.tsx @@ -1,8 +1,14 @@ -import { forwardRef, useEffect, useState, useRef, useImperativeHandle } from 'react' -import style from './Artwork.module.scss' +import { + forwardRef, + useEffect, + useState, + useRef, + useImperativeHandle, +} from "react" +import style from "./Artwork.module.scss" import cs from "classnames" -import { LoaderBlock } from '../Layout/LoaderBlock' -import { Error } from '../Error/Error' +import { LoaderBlock } from "../Layout/LoaderBlock" +import { Error } from "../Error/Error" interface Props { url?: string @@ -16,76 +22,73 @@ export interface ArtworkIframeRef { getHtmlIframe: () => HTMLIFrameElement | null } -export const ArtworkIframe = forwardRef(({ - url, - textWaiting, - onLoaded, - hasLoading = true, -}, ref) => { - const [loading, setLoading] = useState(false) - const [error, setError] = useState(false) - const isLoaded = useRef(false) - const iframeRef = useRef(null) +export const ArtworkIframe = forwardRef( + ({ url, textWaiting, onLoaded, hasLoading = true }, ref) => { + const [loading, setLoading] = useState(false) + const [error, setError] = useState(false) + const isLoaded = useRef(false) + const iframeRef = useRef(null) - useEffect(() => { - setLoading(!isLoaded.current) - setError(false) - }, []) + useEffect(() => { + setLoading(!isLoaded.current) + setError(false) + }, []) + + const reloadIframe = () => { + if (url && iframeRef?.current?.contentWindow) { + setLoading(true) + setError(false) + iframeRef.current.contentWindow.location.replace(url) + } + } - const reloadIframe = () => { - if (url && iframeRef?.current?.contentWindow) { + useEffect(() => { + // when the url changes, we set reload to true setLoading(true) - setError(false) - iframeRef.current.contentWindow.location.replace(url) + if (url && iframeRef?.current?.contentWindow) { + // keep iframe history hidden + iframeRef.current.contentWindow.location.replace(url) + } + }, [url, iframeRef]) + + // set iframe state to loaded and set ref to loaded to prevent loader init to loading + const setIframeLoaded = () => { + isLoaded.current = true + setLoading(false) } - } - useEffect(() => { - // when the url changes, we set reload to true - setLoading(true) - if (url && iframeRef?.current?.contentWindow) { - // keep iframe history hidden - iframeRef.current.contentWindow.location.replace(url) + const getHtmlIframe = (): HTMLIFrameElement | null => { + return iframeRef.current } - }, [url, iframeRef]) - // set iframe state to loaded and set ref to loaded to prevent loader init to loading - const setIframeLoaded = () => { - isLoaded.current = true - setLoading(false) - } + useImperativeHandle(ref, () => ({ + reloadIframe, + getHtmlIframe, + })) - const getHtmlIframe = (): HTMLIFrameElement | null => { - return iframeRef.current + return ( +
+ + src={src} + width="660px" + height="380" + frameBorder="0" + sandbox="allow-same-origin allow-scripts" + allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" + >
- ); -}); -EmbedSpotify.displayName = 'EmbedSpotify'; + ) +}) +EmbedSpotify.displayName = "EmbedSpotify" -export default EmbedSpotify; +export default EmbedSpotify diff --git a/src/components/NFTArticle/elements/Embed/EmbedTwitter.tsx b/src/components/NFTArticle/elements/Embed/EmbedTwitter.tsx index 571835c7b..571b13f2c 100644 --- a/src/components/NFTArticle/elements/Embed/EmbedTwitter.tsx +++ b/src/components/NFTArticle/elements/Embed/EmbedTwitter.tsx @@ -1,70 +1,74 @@ -import React, { memo, useState, useEffect } from 'react'; -import { EmbedElementProps } from "./EmbedMediaDisplay"; -import { getTweetIdFromUrl } from "../../../../utils/embed"; -import style from "./Embed.module.scss"; -import { Error } from "../../../Error/Error"; -import useInit from "../../../../hooks/useInit"; -import Skeleton from "../../../Skeleton"; -import { LoaderBlock } from "../../../Layout/LoaderBlock"; +import React, { memo, useState, useEffect } from "react" +import { EmbedElementProps } from "./EmbedMediaDisplay" +import { getTweetIdFromUrl } from "../../../../utils/embed" +import style from "./Embed.module.scss" +import { Error } from "../../../Error/Error" +import useInit from "../../../../hooks/useInit" +import Skeleton from "../../../Skeleton" +import { LoaderBlock } from "../../../Layout/LoaderBlock" -const twitterWidgetJs = 'https://platform.twitter.com/widgets.js'; +const twitterWidgetJs = "https://platform.twitter.com/widgets.js" declare global { interface Window { - twttr: any; + twttr: any } } -const EmbedTwitter = memo(({ href}) => { - const ref = React.useRef(null); - const [isInit, setIsInit] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [hasTweetNotFound, setHasTweetNotFound] = useState(false); +const EmbedTwitter = memo(({ href }) => { + const ref = React.useRef(null) + const [isInit, setIsInit] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const [hasTweetNotFound, setHasTweetNotFound] = useState(false) useInit(() => { - const script = require('scriptjs'); - script(twitterWidgetJs, 'twitter-embed', () => setIsInit(true)); + const script = require("scriptjs") + script(twitterWidgetJs, "twitter-embed", () => setIsInit(true)) }) useEffect(() => { const loadTweet = async () => { setIsLoading(true) if (ref.current) { - ref.current?.replaceChildren(); + ref.current?.replaceChildren() } const tweetElement = await window.twttr.widgets.createTweet( tweetId, - ref?.current, + ref?.current ) - setIsLoading(false); + setIsLoading(false) if (!tweetElement) { - setHasTweetNotFound(true); + setHasTweetNotFound(true) } - }; + } - if (!isInit) return ; - setHasTweetNotFound(false); - const tweetId = getTweetIdFromUrl(href); - if (!tweetId) return ; + if (!isInit) return + setHasTweetNotFound(false) + const tweetId = getTweetIdFromUrl(href) + if (!tweetId) return if (!window.twttr) { - console.error('Failure to load window.twttr, aborting load'); - return; + console.error("Failure to load window.twttr, aborting load") + return } if (!window.twttr.widgets.createTweet) { - console.error(`Method createTweet is not present anymore in twttr.widget api`); - return; + console.error( + `Method createTweet is not present anymore in twttr.widget api` + ) + return } - loadTweet(); - }, [href, isInit]); + loadTweet() + }, [href, isInit]) return ( <> - {isLoading && } -
+ {isLoading && ( + + )} +
{hasTweetNotFound && No tweet found for this url} - ); -}); + ) +}) -EmbedTwitter.displayName = 'EmbedTwitter'; -export default EmbedTwitter; +EmbedTwitter.displayName = "EmbedTwitter" +export default EmbedTwitter diff --git a/src/components/NFTArticle/elements/Embed/EmbedYoutube.tsx b/src/components/NFTArticle/elements/Embed/EmbedYoutube.tsx index 0336e5cdd..edf176142 100644 --- a/src/components/NFTArticle/elements/Embed/EmbedYoutube.tsx +++ b/src/components/NFTArticle/elements/Embed/EmbedYoutube.tsx @@ -1,29 +1,26 @@ -import React, { memo, useMemo } from 'react' +import React, { memo, useMemo } from "react" import style from "./Embed.module.scss" import cs from "classnames" -import { getYoutubeCodeFromUrl } from "../../../../utils/embed"; -import { EmbedElementProps } from "./EmbedMediaDisplay"; +import { getYoutubeCodeFromUrl } from "../../../../utils/embed" +import { EmbedElementProps } from "./EmbedMediaDisplay" const EmbedYoutube = memo(({ href }) => { const embedUrl = useMemo(() => { - const code = getYoutubeCodeFromUrl(href); - return `https://www.youtube.com/embed/${code}`; - }, [href]); + const code = getYoutubeCodeFromUrl(href) + return `https://www.youtube.com/embed/${code}` + }, [href]) return ( -
+
+ >
- ); -}); -EmbedYoutube.displayName = 'EmbedYoutube'; + ) +}) +EmbedYoutube.displayName = "EmbedYoutube" -export default EmbedYoutube; +export default EmbedYoutube diff --git a/src/components/NFTArticle/elements/Figure/FigcaptionEditor.tsx b/src/components/NFTArticle/elements/Figure/FigcaptionEditor.tsx index 6577d186a..d6e7b19ad 100644 --- a/src/components/NFTArticle/elements/Figure/FigcaptionEditor.tsx +++ b/src/components/NFTArticle/elements/Figure/FigcaptionEditor.tsx @@ -15,9 +15,12 @@ export function FigcaptionElement({ const text = element.children[0].text return ( -
+
{text === "" && (
Image caption... diff --git a/src/components/NFTArticle/elements/Figure/FigureDefinition.tsx b/src/components/NFTArticle/elements/Figure/FigureDefinition.tsx index 613fe72d9..0dd32c815 100644 --- a/src/components/NFTArticle/elements/Figure/FigureDefinition.tsx +++ b/src/components/NFTArticle/elements/Figure/FigureDefinition.tsx @@ -1,26 +1,30 @@ -import { FigureElement } from "./FigureEditor"; -import { ImageAttributeSettings } from "../Image/ImageAttributeSettings"; -import { BlockParamsModal } from "../../SlateEditor/UI/BlockParamsModal"; -import { Node, Transforms } from "slate"; -import { IArticleBlockDefinition, TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition"; -import { FigcaptionElement } from "./FigcaptionEditor"; -import { VideoAttributeSettings } from "../Video/VideoAttributeSettings"; +import { FigureElement } from "./FigureEditor" +import { ImageAttributeSettings } from "../Image/ImageAttributeSettings" +import { BlockParamsModal } from "../../SlateEditor/UI/BlockParamsModal" +import { Node, Transforms } from "slate" +import { + IArticleBlockDefinition, + TEditAttributeComp, +} from "../../../../types/ArticleEditor/BlockDefinition" +import { FigcaptionElement } from "./FigcaptionEditor" +import { VideoAttributeSettings } from "../Video/VideoAttributeSettings" -const medias = ["image", "video"]; +const medias = ["image", "video"] const mediaAttributeSettings: Record = { - "image": ImageAttributeSettings, - "video": VideoAttributeSettings, + image: ImageAttributeSettings, + video: VideoAttributeSettings, } export const figureDefinition: IArticleBlockDefinition = { name: "Figure", icon: null, render: FigureElement, hasUtilityWrapper: true, + hasDeleteBehaviorRemoveBlock: true, editAttributeComp: ({ element, onEdit }) => { const children = Node.elements(element) for (const [child] of children) { if (medias.indexOf(child.type) > -1) { - const AttributeSettings = mediaAttributeSettings[child.type]; + const AttributeSettings = mediaAttributeSettings[child.type] return } } @@ -34,7 +38,7 @@ export const figureDefinition: IArticleBlockDefinition = { for (const [child, childPath] of children) { if (medias.indexOf(child.type) > -1) { Transforms.setNodes(editor, update, { - at: path.concat(childPath) + at: path.concat(childPath), }) return } diff --git a/src/components/NFTArticle/elements/Figure/FigureEditor.tsx b/src/components/NFTArticle/elements/Figure/FigureEditor.tsx index 5315d6c78..96158db56 100644 --- a/src/components/NFTArticle/elements/Figure/FigureEditor.tsx +++ b/src/components/NFTArticle/elements/Figure/FigureEditor.tsx @@ -3,7 +3,6 @@ import { useSelected } from "slate-react" import style from "./FigureEditor.module.scss" import cs from "classnames" - interface Props { attributes: any element: any @@ -16,11 +15,13 @@ export function FigureElement({ const selected = useSelected() return ( -
+
{children}
) } - diff --git a/src/components/NFTArticle/elements/Figure/FigureProcessor.ts b/src/components/NFTArticle/elements/Figure/FigureProcessor.ts index f5189de2a..06f2045ca 100644 --- a/src/components/NFTArticle/elements/Figure/FigureProcessor.ts +++ b/src/components/NFTArticle/elements/Figure/FigureProcessor.ts @@ -1,17 +1,16 @@ -import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor"; -import { Node } from "slate"; -import { convertSlateLeafDirectiveToMarkdown } from "../../processor/getMarkdownFromSlateEditorState"; - +import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor" +import { Node } from "slate" +import { convertSlateLeafDirectiveToMarkdown } from "../../processor/getMarkdownFromSlateEditorState" const createMarkdownImageFromFigure = (nodeFigure: Node, nodeImage: Node) => { // create a regular image node const imageNode: any = { type: "image", - url: nodeImage.url + url: nodeImage.url, } // find if there's a caption - const caption: Node|null = nodeFigure.children.find( + const caption: Node | null = nodeFigure.children.find( (node: Node) => node.type === ("figcaption" as any) ) if (caption && caption.children?.length > 0) { @@ -22,10 +21,10 @@ const createMarkdownImageFromFigure = (nodeFigure: Node, nodeImage: Node) => { const createMarkdownVideoFromFigure = (nodeFigure: Node, nodeVideo: Node) => { const videoNode: Node = { - type: 'video', - src: nodeVideo.src + type: "video", + src: nodeVideo.src, } - const caption: Node|null = nodeFigure.children.find( + const caption: Node | null = nodeFigure.children.find( (node: Node) => node.type === ("figcaption" as any) ) if (caption && caption.children?.length > 0) { @@ -33,9 +32,12 @@ const createMarkdownVideoFromFigure = (nodeFigure: Node, nodeVideo: Node) => { } return convertSlateLeafDirectiveToMarkdown(videoNode) } -const mediasConvert: Record any> = { - "image": createMarkdownImageFromFigure, - "video": createMarkdownVideoFromFigure +const mediasConvert: Record< + string, + (nodeFigure: Node, nodeMedia: Node) => any +> = { + image: createMarkdownImageFromFigure, + video: createMarkdownVideoFromFigure, } export const figureProcessor: IArticleElementProcessor = { @@ -44,9 +46,9 @@ export const figureProcessor: IArticleElementProcessor = { * in proper markdown */ transformSlateToMarkdownMdhast: (node) => { - const mediaNode: Node|null = node.children.find( + const mediaNode: Node | null = node.children.find( (node: Node) => ["image", "video"].indexOf(node.type) > -1 ) - return mediasConvert[mediaNode.type](node, mediaNode); - } + return mediasConvert[mediaNode.type](node, mediaNode) + }, } diff --git a/src/components/NFTArticle/elements/Heading/HeadingAttributeSettings.tsx b/src/components/NFTArticle/elements/Heading/HeadingAttributeSettings.tsx index 60ed4a15c..1c04b247e 100644 --- a/src/components/NFTArticle/elements/Heading/HeadingAttributeSettings.tsx +++ b/src/components/NFTArticle/elements/Heading/HeadingAttributeSettings.tsx @@ -1,6 +1,6 @@ import cs from "classnames" import { ContextualMenuItems } from "../../../Menus/ContextualMenuItems" -import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition"; +import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition" export const HeadingAttributeSettings: TEditAttributeComp = ({ element, @@ -12,15 +12,17 @@ export const HeadingAttributeSettings: TEditAttributeComp = ({ ))} diff --git a/src/components/NFTArticle/elements/Heading/HeadingDefinition.tsx b/src/components/NFTArticle/elements/Heading/HeadingDefinition.tsx index 5ee2508c5..62029b18c 100644 --- a/src/components/NFTArticle/elements/Heading/HeadingDefinition.tsx +++ b/src/components/NFTArticle/elements/Heading/HeadingDefinition.tsx @@ -1,27 +1,27 @@ -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; -import { EBreakBehavior } from "../../SlateEditor/Plugins/SlateBreaksPlugin"; -import { HeadingAttributeSettings } from "./HeadingAttributeSettings"; +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" +import { EBreakBehavior } from "../../SlateEditor/Plugins/SlateBreaksPlugin" +import { HeadingAttributeSettings } from "./HeadingAttributeSettings" export const headingDefinition: IArticleBlockDefinition = { name: "Heading", - icon: , + icon: , buttonInstantiable: true, render: ({ attributes, element, children }) => { switch (element.depth) { case 1: - return

{children}

; + return

{children}

case 2: - return

{children}

; + return

{children}

case 3: - return

{children}

; + return

{children}

case 4: - return

{children}

; + return

{children}

case 5: - return
{children}
; + return
{children}
case 6: - return
{children}
; + return
{children}
default: - break; + break } }, insertBreakBehavior: EBreakBehavior.insertParagraph, @@ -29,9 +29,11 @@ export const headingDefinition: IArticleBlockDefinition = { instanciateElement: () => ({ type: "heading", depth: 1, - children: [{ - text: "" - }] + children: [ + { + text: "", + }, + ], }), editAttributeComp: HeadingAttributeSettings, -}; +} diff --git a/src/components/NFTArticle/elements/Image/ImageAttributeSettings.tsx b/src/components/NFTArticle/elements/Image/ImageAttributeSettings.tsx index 232fa2ab7..9c320158c 100644 --- a/src/components/NFTArticle/elements/Image/ImageAttributeSettings.tsx +++ b/src/components/NFTArticle/elements/Image/ImageAttributeSettings.tsx @@ -8,15 +8,15 @@ import { useCallback, useState } from "react" import { Field } from "../../../Form/Field" import { Submit } from "../../../Form/Submit" import { Button } from "../../../Button" -import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition"; +import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition" const tabs: TabDefinition[] = [ { - name: "Upload" + name: "Upload", }, { - name: "External link" - } + name: "External link", + }, ] export const ImageAttributeSettings: TEditAttributeComp = ({ @@ -28,13 +28,13 @@ export const ImageAttributeSettings: TEditAttributeComp = ({ const onAddFile = (file: File) => { if (file) { onEdit({ - url: URL.createObjectURL(file) + url: URL.createObjectURL(file), }) } } const handleClickImport = useCallback(() => { onEdit({ - url: textUrl + url: textUrl, }) }, [onEdit, textUrl]) @@ -48,35 +48,29 @@ export const ImageAttributeSettings: TEditAttributeComp = ({
{tabIndex === 0 ? ( files && files[0] && onAddFile(files[0])} + onChange={(files) => files && files[0] && onAddFile(files[0])} accepted={["image/jpeg", "image/png", "image/gif"]} className={cs(style.dropzone)} - textDefault={( + textDefault={
- - - Import an image - + + Import an image (20mb max)
- )} - textDrag={( + } + textDrag={
- - Drop image file - + Drop image file
- )} + } /> - ):tabIndex === 1 ? ( -
+ ) : tabIndex === 1 ? ( + setTextUrl(event.target.value)} + onChange={(event) => setTextUrl(event.target.value)} placeholder="https://image.com/image.jpg" /> @@ -93,7 +87,7 @@ export const ImageAttributeSettings: TEditAttributeComp = ({
- ):null} + ) : null}
)} diff --git a/src/components/NFTArticle/elements/Image/ImageDefinition.tsx b/src/components/NFTArticle/elements/Image/ImageDefinition.tsx index 6bc8ea89d..9f4ed7949 100644 --- a/src/components/NFTArticle/elements/Image/ImageDefinition.tsx +++ b/src/components/NFTArticle/elements/Image/ImageDefinition.tsx @@ -1,5 +1,5 @@ -import { ImageEditor } from "./ImageEditor"; -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; +import { ImageEditor } from "./ImageEditor" +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" interface InstanciateImageOpts { url?: string @@ -7,23 +7,30 @@ interface InstanciateImageOpts { } export const imageDefinition: IArticleBlockDefinition = { name: "Image", - icon: , + icon: , buttonInstantiable: true, render: ImageEditor, hasUtilityWrapper: false, - instanciateElement: (opts = { url: '', caption: '' }) => ({ + instanciateElement: (opts = { url: "", caption: "" }) => ({ type: "figure", - children: [{ - type: "image", - url: opts.url, // if "", will display the "add image" component - children: [{ - text: "" - }] - }, { - type: "figcaption", - children: [{ - text: opts.caption - }] - }] + children: [ + { + type: "image", + url: opts.url, // if "", will display the "add image" component + children: [ + { + text: "", + }, + ], + }, + { + type: "figcaption", + children: [ + { + text: opts.caption, + }, + ], + }, + ], }), } diff --git a/src/components/NFTArticle/elements/Image/ImageDisplay.tsx b/src/components/NFTArticle/elements/Image/ImageDisplay.tsx index 17a57ab0f..c6470c92f 100644 --- a/src/components/NFTArticle/elements/Image/ImageDisplay.tsx +++ b/src/components/NFTArticle/elements/Image/ImageDisplay.tsx @@ -4,20 +4,11 @@ interface Props { src: string alt: string } -export function ImageDisplay({ - src, - alt, -}: Props) { +export function ImageDisplay({ src, alt }: Props) { return (
- - {alt && ( -
- {alt} -
- )} + + {alt &&
{alt}
}
) } diff --git a/src/components/NFTArticle/elements/Image/ImageEditor.tsx b/src/components/NFTArticle/elements/Image/ImageEditor.tsx index 8b53aa5d9..7465ca046 100644 --- a/src/components/NFTArticle/elements/Image/ImageEditor.tsx +++ b/src/components/NFTArticle/elements/Image/ImageEditor.tsx @@ -25,7 +25,7 @@ export function ImageEditor({ const setImage = (element: any) => { Transforms.setNodes(editor, element, { - at: path + at: path, }) } @@ -35,10 +35,8 @@ export function ImageEditor({ {children}
{hasUrl ? ( - - ):( + + ) : ( )}
{showAddImage && ( - setShowAddImage(false)} - > + setShowAddImage(false)}> { + onEdit={(element) => { setImage(element) setShowAddImage(false) }} diff --git a/src/components/NFTArticle/elements/Image/ImageProcessor.ts b/src/components/NFTArticle/elements/Image/ImageProcessor.ts index 60c70e229..6ae9524ee 100644 --- a/src/components/NFTArticle/elements/Image/ImageProcessor.ts +++ b/src/components/NFTArticle/elements/Image/ImageProcessor.ts @@ -1,21 +1,28 @@ -import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor"; +import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor" export const imageProcessor: IArticleElementProcessor = { transformMarkdownMdhastToSlate: (node) => { - return ({ + return { type: "figure", - children: [{ - type: "image", - url: node.url, - children: [{ - text: "" - }], - }, { - type: "figcaption", - children: [{ - text: node.alt - }] - }] - }); - } + children: [ + { + type: "image", + url: node.url, + children: [ + { + text: "", + }, + ], + }, + { + type: "figcaption", + children: [ + { + text: node.alt, + }, + ], + }, + ], + } + }, } diff --git a/src/components/NFTArticle/elements/Link/LinkDefinition.tsx b/src/components/NFTArticle/elements/Link/LinkDefinition.tsx index e144dfdc3..c52c1a023 100644 --- a/src/components/NFTArticle/elements/Link/LinkDefinition.tsx +++ b/src/components/NFTArticle/elements/Link/LinkDefinition.tsx @@ -1,16 +1,50 @@ -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" + +export function sanitizeUrl(url: string) { + if (!url) { + return "about:blank" + } + + var invalidProtocolRegex = /^(%20|\s)*(javascript|data|vbscript)/im + var ctrlCharactersRegex = /[^\x20-\x7EÀ-ž]/gim + var urlSchemeRegex = /^([^:]+):/gm + var relativeFirstCharacters = [".", "/"] + + function _isRelativeUrlWithoutProtocol(url: string) { + return relativeFirstCharacters.indexOf(url[0]) > -1 + } + + var sanitizedUrl = url.replace(ctrlCharactersRegex, "").trim() + if (_isRelativeUrlWithoutProtocol(sanitizedUrl)) { + return sanitizedUrl + } + + var urlSchemeParseResults = sanitizedUrl.match(urlSchemeRegex) + if (!urlSchemeParseResults) { + return sanitizedUrl + } + + var urlScheme = urlSchemeParseResults[0] + if (invalidProtocolRegex.test(urlScheme)) { + return "about:blank" + } + + return sanitizedUrl +} export const linkDefinition: IArticleBlockDefinition = { name: "Link", - icon: , - render: ({ attributes, element, children }) => ( - - {children} - - ), + icon: , + render: ({ attributes, element, children }) => { + return ( + + {children} + + ) + }, hasUtilityWrapper: false, } diff --git a/src/components/NFTArticle/elements/Link/LinkElement.tsx b/src/components/NFTArticle/elements/Link/LinkElement.tsx new file mode 100644 index 000000000..eb30f09f2 --- /dev/null +++ b/src/components/NFTArticle/elements/Link/LinkElement.tsx @@ -0,0 +1,10 @@ +import { PropsWithChildren, useMemo } from "react" +import { sanitizeUrl } from "./LinkDefinition" + +interface Props { + href: string +} +export function LinkElement({ href, children }: PropsWithChildren) { + const sanitized = useMemo(() => sanitizeUrl(href), [href]) + return {children} +} diff --git a/src/components/NFTArticle/elements/List/ListAttributeSettings.tsx b/src/components/NFTArticle/elements/List/ListAttributeSettings.tsx index c8de2ebb9..b9a3702cb 100644 --- a/src/components/NFTArticle/elements/List/ListAttributeSettings.tsx +++ b/src/components/NFTArticle/elements/List/ListAttributeSettings.tsx @@ -1,6 +1,6 @@ import cs from "classnames" import { ContextualMenuItems } from "../../../Menus/ContextualMenuItems" -import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition"; +import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition" export const ListAttributeSettings: TEditAttributeComp = ({ element, @@ -10,26 +10,30 @@ export const ListAttributeSettings: TEditAttributeComp = ({ diff --git a/src/components/NFTArticle/elements/List/ListDefinition.tsx b/src/components/NFTArticle/elements/List/ListDefinition.tsx index 73a2c1c38..6a4a2f328 100644 --- a/src/components/NFTArticle/elements/List/ListDefinition.tsx +++ b/src/components/NFTArticle/elements/List/ListDefinition.tsx @@ -1,36 +1,39 @@ -import { ListAttributeSettings } from "./ListAttributeSettings"; -import { Editor, Element, Node, Path, Range, Transforms } from "slate"; -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; +import { ListAttributeSettings } from "./ListAttributeSettings" +import { Editor, Element, Node, Path, Range, Transforms } from "slate" +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" export const listDefinition: IArticleBlockDefinition = { name: "List", - icon: , + icon: , buttonInstantiable: true, - render: ({ attributes, element, children }) => ( + render: ({ attributes, element, children }) => element.ordered ? (
    {children}
- ):( + ) : (
    {children}
- ) - ), + ), hasUtilityWrapper: true, instanciateElement: () => ({ type: "list", ordered: false, spread: false, - children: [{ - type: "listItem", - children: [{ - text: "" - }] - }] + children: [ + { + type: "listItem", + children: [ + { + text: "", + }, + ], + }, + ], }), editAttributeComp: ListAttributeSettings, } export const listItemDefinition: IArticleBlockDefinition = { name: "List Item", - icon: , + icon: , render: ({ attributes, element, children }) => (
  • {element.checked === true ? ( @@ -42,23 +45,30 @@ export const listItemDefinition: IArticleBlockDefinition = {
  • ), insertBreakBehavior: (editor, element) => { - const { selection } = editor; - if (selection && !Range.isCollapsed(selection)) return true; - const [nodeListItem, pathListItem] = element; - const text = Node.string(nodeListItem); - if (text) return true; + const { selection } = editor + if (selection && !Range.isCollapsed(selection)) return true + const [nodeListItem, pathListItem] = element + const text = Node.string(nodeListItem) + if (text) return true + const nextLi = Path.next(pathListItem) + const hasNextLi = Node.has(editor, nextLi) + if (hasNextLi) return true const parentList = Editor.above(editor, { at: pathListItem, - match: n => - !Editor.isEditor(n) && Element.isElement(n) && n.type === 'list', - mode: 'lowest', + match: (n) => + !Editor.isEditor(n) && Element.isElement(n) && n.type === "list", + mode: "lowest", }) - if (!parentList) return true; + if (!parentList) return true const [, pathParentList] = parentList - const next = Path.next(pathParentList); - Transforms.setNodes(editor, { type: 'paragraph' }, { - at: pathListItem, - }) + const next = Path.next(pathParentList) + Transforms.setNodes( + editor, + { type: "paragraph" }, + { + at: pathListItem, + } + ) Transforms.moveNodes(editor, { at: pathListItem, to: next, diff --git a/src/components/NFTArticle/elements/Math/BlockKatexEditor.tsx b/src/components/NFTArticle/elements/Math/BlockKatexEditor.tsx index 317e95c95..ea04eea21 100644 --- a/src/components/NFTArticle/elements/Math/BlockKatexEditor.tsx +++ b/src/components/NFTArticle/elements/Math/BlockKatexEditor.tsx @@ -1,56 +1,82 @@ -import React, { memo, PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'; -import Slate, { Transforms } from 'slate'; -import style from "./BlockKatex.module.scss"; -import { ReactEditor, RenderElementProps, useSlateStatic, useSelected } from "slate-react"; -import { Katex } from "./Katex"; -import TextareaAutosize from "react-textarea-autosize"; -import cs from "classnames"; -import useClickOutside from "../../../../hooks/useClickOutside"; +import React, { + memo, + PropsWithChildren, + useCallback, + useEffect, + useRef, + useState, +} from "react" +import Slate, { Transforms } from "slate" +import style from "./BlockKatex.module.scss" +import { + ReactEditor, + RenderElementProps, + useSlateStatic, + useSelected, +} from "slate-react" +import { Katex } from "./Katex" +import TextareaAutosize from "react-textarea-autosize" +import cs from "classnames" +import useClickOutside from "../../../../hooks/useClickOutside" interface BlockKatexEditorProps { slateAttributes?: RenderElementProps["attributes"] slateElement: Slate.Element } -const _BlockKatexEditor = ({ slateElement }: PropsWithChildren) => { - const refTextArea = useRef(null); - const refContainer = useRef(null); - const [isFocused, setIsFocused] = useState(false); - const editor = useSlateStatic(); - const path = ReactEditor.findPath(editor, slateElement); - const selected = useSelected(); +const _BlockKatexEditor = ({ + slateElement, +}: PropsWithChildren) => { + const refTextArea = useRef(null) + const refContainer = useRef(null) + const [isFocused, setIsFocused] = useState(false) + const editor = useSlateStatic() + const path = ReactEditor.findPath(editor, slateElement) + const selected = useSelected() const handleClickKatex = useCallback(() => { - if (isFocused) return; - setIsFocused(true); + if (isFocused) return + setIsFocused(true) if (refTextArea.current) { - const end = refTextArea.current.value.length; - refTextArea.current.setSelectionRange(end, end); - refTextArea.current.focus(); + const end = refTextArea.current.value.length + refTextArea.current.setSelectionRange(end, end) + refTextArea.current.focus() } }, [isFocused]) - const handleFocus = useCallback(() => setIsFocused(true), []); - const handleChange = useCallback>((e) => { - const newMath = e.target.value; - Transforms.setNodes(editor, { math: newMath },{ - at: path - }) - }, [editor, path]) - useClickOutside(refContainer, () => { - setTimeout(() => { - setIsFocused(false); - }, 100) - }, !isFocused) - + const handleFocus = useCallback(() => setIsFocused(true), []) + const handleChange = useCallback< + React.ChangeEventHandler + >( + (e) => { + const newMath = e.target.value + Transforms.setNodes( + editor, + { math: newMath }, + { + at: path, + } + ) + }, + [editor, path] + ) + useClickOutside( + refContainer, + () => { + setTimeout(() => { + setIsFocused(false) + }, 100) + }, + !isFocused + ) + useEffect(() => { if (selected && refTextArea.current) { - const end = refTextArea.current.value.length; - refTextArea.current.setSelectionRange(end, end); - refTextArea.current.focus(); + const end = refTextArea.current.value.length + refTextArea.current.setSelectionRange(end, end) + refTextArea.current.focus() } }, [selected]) - - const { math } = slateElement; + const { math } = slateElement return (
    - {math && + {math && (
    + [style.container_katex_margin]: isFocused, + })} + > {math}
    - } + )}
    - ); -}; + ) +} -export const BlockKatexEditor = memo(_BlockKatexEditor); +export const BlockKatexEditor = memo(_BlockKatexEditor) diff --git a/src/components/NFTArticle/elements/Math/Katex.tsx b/src/components/NFTArticle/elements/Math/Katex.tsx index f8ceb9f74..ccbc415f5 100644 --- a/src/components/NFTArticle/elements/Math/Katex.tsx +++ b/src/components/NFTArticle/elements/Math/Katex.tsx @@ -1,5 +1,5 @@ -import React, { memo, useMemo } from 'react' -import KaTeX from 'katex' +import React, { memo, useMemo } from "react" +import KaTeX from "katex" import style from "./BlockKatex.module.scss" import cs from "classnames" @@ -13,16 +13,16 @@ const _Katex = ({ children, inline }: KatexProps) => { const generatedHtml = KaTeX.renderToString(children, { displayMode: !inline, throwOnError: false, - }); - return { __html: generatedHtml }; - }, [children, inline]); + }) + return { __html: generatedHtml } + }, [children, inline]) return ( - ); -}; + ) +} -export const Katex = memo(_Katex); +export const Katex = memo(_Katex) diff --git a/src/components/NFTArticle/elements/Math/MathDefinition.tsx b/src/components/NFTArticle/elements/Math/MathDefinition.tsx index ef232da3e..72dc4836f 100644 --- a/src/components/NFTArticle/elements/Math/MathDefinition.tsx +++ b/src/components/NFTArticle/elements/Math/MathDefinition.tsx @@ -1,34 +1,34 @@ -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; -import style from "../../NFTArticle.module.scss"; -import { BlockKatexEditor } from "./BlockKatexEditor"; +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" +import style from "../../NFTArticle.module.scss" +import { BlockKatexEditor } from "./BlockKatexEditor" export const mathDefinition: IArticleBlockDefinition = { name: "Math", - icon: , + icon: , buttonInstantiable: true, render: ({ attributes, element, children }) => (
    {children} - +
    ), hasUtilityWrapper: true, instanciateElement: () => ({ type: "math", math: "", - children: [{ - text: "" - }] - }) + children: [ + { + text: "", + }, + ], + }), } export const inlineMathDefinition: IArticleBlockDefinition = { name: "Math", - icon: , + icon: , render: ({ attributes, element, children }) => ( - - inline math - + inline math ), hasUtilityWrapper: false, } diff --git a/src/components/NFTArticle/elements/Math/MathProcessor.ts b/src/components/NFTArticle/elements/Math/MathProcessor.ts index 443c0e38b..00b8f7337 100644 --- a/src/components/NFTArticle/elements/Math/MathProcessor.ts +++ b/src/components/NFTArticle/elements/Math/MathProcessor.ts @@ -1,15 +1,15 @@ -import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor"; +import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor" export const mathProcessor: IArticleElementProcessor = { transformMarkdownMdhastToSlate: (node: any) => { return { type: node.type, - children: [{ text: '' }], + children: [{ text: "" }], math: node.value, } }, transformSlateToMarkdownMdhast: (node: any) => ({ type: node.type, value: node.math, - }) + }), } diff --git a/src/components/NFTArticle/elements/Mention/FloatingMentionMenu.module.scss b/src/components/NFTArticle/elements/Mention/FloatingMentionMenu.module.scss new file mode 100644 index 000000000..8512c92f5 --- /dev/null +++ b/src/components/NFTArticle/elements/Mention/FloatingMentionMenu.module.scss @@ -0,0 +1,11 @@ +.floating_menu { + top: -9999px; + left: -9999px; + position: absolute; + z-index: 2; + background: var(--color-white); +} +.search_results { + border: 2px solid var(--color-gray-vvlight) !important; + left: -2px !important; +} diff --git a/src/components/NFTArticle/elements/Mention/FloatingMentionMenu.tsx b/src/components/NFTArticle/elements/Mention/FloatingMentionMenu.tsx new file mode 100644 index 000000000..a7bd68b36 --- /dev/null +++ b/src/components/NFTArticle/elements/Mention/FloatingMentionMenu.tsx @@ -0,0 +1,140 @@ +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react" +import { ReactEditor, useSlate } from "slate-react" +import { Editor, Range, Transforms } from "slate" +import ReactDOM from "react-dom" +import { InputSearchUser } from "../../../Input/InputSearchUser" +import { mentionDefinition } from "./MentionDefinition" +import style from "./FloatingMentionMenu.module.scss" +import { User } from "../../../../types/entities/User" + +export interface RefFloatingMentionMenu { + onKeyDown: (event: React.KeyboardEvent) => void +} +export const FloatingMentionMenu = forwardRef( + (props, ref) => { + const editor = useSlate() + const refFloatingMenu = useRef(null) + const [target, setTarget] = useState() + const [index, setIndex] = useState(0) + const [search, setSearch] = useState("") + const [fetchedUsers, setFetchedUsers] = useState([]) + + const handleFetchUsers = useCallback((users) => { + setIndex(0) + setFetchedUsers(users) + }, []) + const handleSelectUser = useCallback( + (tzAddress: string) => { + if (target) { + Transforms.select(editor, target) + } + const mention = mentionDefinition.instanciateElement!({ tzAddress }) + Transforms.insertNodes(editor, mention) + Transforms.move(editor) + setTarget(null) + }, + [editor, target] + ) + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (target) { + switch (event.key) { + case "ArrowDown": + event.preventDefault() + const prevIndex = index >= fetchedUsers.length - 1 ? 0 : index + 1 + setIndex(prevIndex) + break + case "ArrowUp": + event.preventDefault() + const nextIndex = index <= 0 ? fetchedUsers.length - 1 : index - 1 + setIndex(nextIndex) + break + case "Tab": + case "Enter": + event.preventDefault() + const selectedUser = fetchedUsers[index] + if (selectedUser) { + handleSelectUser(selectedUser.id) + } + setTarget(null) + break + case "Escape": + event.preventDefault() + setTarget(null) + break + } + } + }, + [fetchedUsers, handleSelectUser, index, target] + ) + + useEffect(() => { + const { selection } = editor + + if (selection && Range.isCollapsed(selection)) { + const [start] = Range.edges(selection) + const wordBefore = Editor.before(editor, start, { unit: "word" }) + const before = wordBefore && Editor.before(editor, wordBefore) + const beforeRange = before && Editor.range(editor, before, start) + const beforeText = beforeRange && Editor.string(editor, beforeRange) + const beforeMatch = beforeText && beforeText.match(/^@(\w+)$/) + const after = Editor.after(editor, start) + const afterRange = Editor.range(editor, start, after) + const afterText = Editor.string(editor, afterRange) + const afterMatch = afterText.match(/^(\s|$)/) + if (beforeMatch && afterMatch) { + setTarget(beforeRange) + setSearch(beforeMatch[1]) + setIndex(0) + return + } + } + setTarget(null) + }, [editor, editor.selection]) + + useEffect(() => { + if (target && search.length > 2) { + const el = refFloatingMenu.current + const domRange = ReactEditor.toDOMRange(editor, target) + const rect = domRange.getBoundingClientRect() + if (el) { + el.style.top = `${rect.top + window.pageYOffset + 24}px` + el.style.left = `${rect.left + window.pageXOffset}px` + } + } + }, [search.length, editor, index, search, target]) + useImperativeHandle( + ref, + () => ({ + onKeyDown: handleKeyDown, + }), + [handleKeyDown] + ) + + return target + ? ReactDOM.createPortal( +
    + +
    , + document.body + ) + : null + } +) +FloatingMentionMenu.displayName = "FloatingMentionMenu" diff --git a/src/components/NFTArticle/elements/Mention/MentionDefinition.tsx b/src/components/NFTArticle/elements/Mention/MentionDefinition.tsx new file mode 100644 index 000000000..d3eeba2ee --- /dev/null +++ b/src/components/NFTArticle/elements/Mention/MentionDefinition.tsx @@ -0,0 +1,19 @@ +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" +import { MentionEditor } from "./MentionEditor" + +interface InstanciateMentionOpts { + tzAddress?: string +} +export const mentionDefinition: IArticleBlockDefinition = + { + name: "Mention", + icon: , + render: MentionEditor, + hasUtilityWrapper: false, + inlineMenu: null, + instanciateElement: ({ tzAddress } = { tzAddress: "" }) => ({ + type: "mention", + tzAddress, + children: [{ text: "" }], + }), + } diff --git a/src/components/NFTArticle/elements/Mention/MentionDisplay.tsx b/src/components/NFTArticle/elements/Mention/MentionDisplay.tsx new file mode 100644 index 000000000..3e6a85fbe --- /dev/null +++ b/src/components/NFTArticle/elements/Mention/MentionDisplay.tsx @@ -0,0 +1,25 @@ +import React, { memo } from "react" +import { UserFromAddress } from "../../../User/UserFromAddress" +import style from "./MentionEditor.module.scss" +import cs from "classnames" +import Link from "next/link" +import { getUserName, getUserProfileLink } from "../../../../utils/user" + +interface MentionDisplayProps { + tzAddress: string +} + +const _MentionDisplay = ({ tzAddress }: MentionDisplayProps) => ( + + {({ user }) => ( + + + {`@`} + {getUserName(user)} + + + )} + +) + +export const MentionDisplay = memo(_MentionDisplay) diff --git a/src/components/NFTArticle/elements/Mention/MentionEditor.module.scss b/src/components/NFTArticle/elements/Mention/MentionEditor.module.scss new file mode 100644 index 000000000..677b68767 --- /dev/null +++ b/src/components/NFTArticle/elements/Mention/MentionEditor.module.scss @@ -0,0 +1,16 @@ +@import "../../../../styles/fonts"; +.mention_user { + vertical-align: bottom; +} + +.mention_display { + padding: 0 4px; + background: var(--color-gray-vvvlight); + border-radius: 4px; + font-weight: 600; + color: var(--color-primary); +} + +.mention_focused { + box-shadow: 0 0 0 2px var(--color-primary); +} diff --git a/src/components/NFTArticle/elements/Mention/MentionEditor.tsx b/src/components/NFTArticle/elements/Mention/MentionEditor.tsx new file mode 100644 index 000000000..c01b0ae38 --- /dev/null +++ b/src/components/NFTArticle/elements/Mention/MentionEditor.tsx @@ -0,0 +1,27 @@ +import { RenderElementProps, useFocused, useSelected } from "slate-react" +import style from "./MentionEditor.module.scss" +import { UserFromAddress } from "../../../User/UserFromAddress" +import cs from "classnames" +import { UserBadge } from "../../../User/UserBadge" +import { MentionDisplay } from "./MentionDisplay" + +export const MentionEditor = ({ + attributes, + children, + element, +}: RenderElementProps) => { + const selected = useSelected() + const focused = useFocused() + return ( + + {children} + + + ) +} diff --git a/src/components/NFTArticle/elements/Mention/MentionProcessor.ts b/src/components/NFTArticle/elements/Mention/MentionProcessor.ts new file mode 100644 index 000000000..2d33a358c --- /dev/null +++ b/src/components/NFTArticle/elements/Mention/MentionProcessor.ts @@ -0,0 +1,20 @@ +import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor" + +export const mentionProcessor: IArticleElementProcessor = { + transformSlateToMarkdownMdhast: (node: any) => { + return { + type: "text", + value: `@${node.tzAddress}`, + } + }, + transformMarkdownMdhastToSlate: (node: any) => ({ + type: node.type, + children: [{ text: "" }], + tzAddress: node.value, + }), + transformMdhastToComponent: (node, properties) => { + return { + tzAddress: properties.value, + } + }, +} diff --git a/src/components/NFTArticle/elements/Paragraph/ParagraphDefinition.tsx b/src/components/NFTArticle/elements/Paragraph/ParagraphDefinition.tsx index 2b735ce0b..2b43a1f1f 100644 --- a/src/components/NFTArticle/elements/Paragraph/ParagraphDefinition.tsx +++ b/src/components/NFTArticle/elements/Paragraph/ParagraphDefinition.tsx @@ -1,8 +1,8 @@ -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" export const paragraphDefinition: IArticleBlockDefinition = { name: "Paragraph", - icon: , + icon: , buttonInstantiable: true, render: ({ attributes, element, children }) => (

    {children}

    @@ -10,8 +10,10 @@ export const paragraphDefinition: IArticleBlockDefinition = { hasUtilityWrapper: true, instanciateElement: () => ({ type: "paragraph", - children: [{ - text: "" - }] + children: [ + { + text: "", + }, + ], }), } diff --git a/src/components/NFTArticle/elements/Table/TableCellEditor.tsx b/src/components/NFTArticle/elements/Table/TableCellEditor.tsx index 07b89ac2e..1f31526fa 100644 --- a/src/components/NFTArticle/elements/Table/TableCellEditor.tsx +++ b/src/components/NFTArticle/elements/Table/TableCellEditor.tsx @@ -1,30 +1,43 @@ -import React, { useMemo } from 'react'; +import React, { useMemo } from "react" import style from "./TableEditor.module.scss" -import { ReactEditor, RenderElementProps, useSelected, useSlate } from "slate-react"; -import cs from "classnames"; -import { Path, Node } from "slate"; +import { + ReactEditor, + RenderElementProps, + useSelected, + useSlate, +} from "slate-react" +import cs from "classnames" +import { Path, Node } from "slate" -export const TableCellEditor = ({ attributes, element, children }: RenderElementProps) => { - const editor = useSlate(); - const path = ReactEditor.findPath(editor, element); - const isSelected = useSelected(); - const align = useMemo<'left'|'center'|'right'>(() => { - const row = path[path.length - 1]; - const table = Path.parent(Path.parent(path)); - const nodeTable = Node.get(editor, table); - if (nodeTable.align) return nodeTable.align[row]; - return 'left'; +export const TableCellEditor = ({ + attributes, + element, + children, +}: RenderElementProps) => { + const editor = useSlate() + const path = ReactEditor.findPath(editor, element) + const isSelected = useSelected() + const align = useMemo<"left" | "center" | "right">(() => { + const row = path[path.length - 1] + const table = Path.parent(Path.parent(path)) + const nodeTable = Node.get(editor, table) + if (nodeTable.align) return nodeTable.align[row] + return "left" }, [editor, path]) const isHeader = useMemo(() => { - const pathParent = Path.parent(path); - const trIdx = pathParent[pathParent.length - 1]; - return trIdx === 0; + const pathParent = Path.parent(path) + const trIdx = pathParent[pathParent.length - 1] + return trIdx === 0 }, [path]) - const Cell = isHeader ? 'th' : 'td'; + const Cell = isHeader ? "th" : "td" return ( - + {children} ) diff --git a/src/components/NFTArticle/elements/Table/TableColToolbar.tsx b/src/components/NFTArticle/elements/Table/TableColToolbar.tsx index 800ceb2f2..1839686ea 100644 --- a/src/components/NFTArticle/elements/Table/TableColToolbar.tsx +++ b/src/components/NFTArticle/elements/Table/TableColToolbar.tsx @@ -1,47 +1,63 @@ -import React, { memo, MouseEventHandler, useCallback } from 'react'; -import style from "./TableEditor.module.scss"; +import React, { memo, MouseEventHandler, useCallback } from "react" +import style from "./TableEditor.module.scss" import effects from "../../../../styles/Effects.module.scss" -import Slate, { Editor } from "slate"; -import { SlateTable } from "../../SlateEditor/Plugins/SlateTablePlugin"; -import cs from "classnames"; +import Slate, { Editor } from "slate" +import { SlateTable } from "../../SlateEditor/Plugins/SlateTablePlugin" +import cs from "classnames" -type Alignment = 'left'|'center'|'right'; -type ToggleColAlignment = (newAlign: Alignment) => MouseEventHandler -const alignments: Alignment[] = ['left', 'center', 'right']; +type Alignment = "left" | "center" | "right" +type ToggleColAlignment = ( + newAlign: Alignment +) => MouseEventHandler +const alignments: Alignment[] = ["left", "center", "right"] interface TableColToolbarProps { - col: number, + col: number row: number tableElement: Slate.Element editor: Editor } -const _TableColToolbar = ({ col, row, editor, tableElement }: TableColToolbarProps) => { - const { cols, rows } = SlateTable.getTableInfos(tableElement); - const styleContainer = { left: `${col * 100 / cols}%` }; - const colAlign = tableElement.align[col]; - const handleToggleColAlignment = useCallback((newAlign) => (e) => { - e.preventDefault() - SlateTable.setColAlignment(editor, tableElement, col, newAlign); - }, [col, editor, tableElement]) - const handleDeleteCol = useCallback((e) => { - e.preventDefault(); - if (cols > 1) { - SlateTable.deleteCol(editor, tableElement, col); - } - }, [col, cols, editor, tableElement]) - const handleDeleteRow = useCallback((e) => { - e.preventDefault(); - if (rows > 1) { - SlateTable.deleteRow(editor, tableElement, row); - } - }, [editor, row, rows, tableElement]) +const _TableColToolbar = ({ + col, + row, + editor, + tableElement, +}: TableColToolbarProps) => { + const { cols, rows } = SlateTable.getTableInfos(tableElement) + const styleContainer = { left: `${(col * 100) / cols}%` } + const colAlign = tableElement.align[col] + const handleToggleColAlignment = useCallback( + (newAlign) => (e) => { + e.preventDefault() + SlateTable.setColAlignment(editor, tableElement, col, newAlign) + }, + [col, editor, tableElement] + ) + const handleDeleteCol = useCallback( + (e) => { + e.preventDefault() + if (cols > 1) { + SlateTable.deleteCol(editor, tableElement, col) + } + }, + [col, cols, editor, tableElement] + ) + const handleDeleteRow = useCallback( + (e) => { + e.preventDefault() + if (rows > 1) { + SlateTable.deleteRow(editor, tableElement, row) + } + }, + [editor, row, rows, tableElement] + ) return (
    - {alignments.map((alignment) => + {alignments.map((alignment) => ( - )} - {cols > 1 && + ))} + {cols > 1 && ( - } - {rows > 1 && + )} + {rows > 1 && ( - } + )}
    - ); -}; + ) +} -export const TableColToolbar = memo(_TableColToolbar); +export const TableColToolbar = memo(_TableColToolbar) diff --git a/src/components/NFTArticle/elements/Table/TableDefinition.tsx b/src/components/NFTArticle/elements/Table/TableDefinition.tsx index d63795bdd..d7d67285d 100644 --- a/src/components/NFTArticle/elements/Table/TableDefinition.tsx +++ b/src/components/NFTArticle/elements/Table/TableDefinition.tsx @@ -1,11 +1,11 @@ -import { TableEditor } from "./TableEditor"; -import { SlateTable } from "../../SlateEditor/Plugins/SlateTablePlugin"; -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; -import { TableCellEditor } from "./TableCellEditor"; +import { TableEditor } from "./TableEditor" +import { SlateTable } from "../../SlateEditor/Plugins/SlateTablePlugin" +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" +import { TableCellEditor } from "./TableCellEditor" export const tableDefinition: IArticleBlockDefinition = { name: "Table", - icon: , + icon: , buttonInstantiable: true, render: ({ attributes, element, children }) => ( @@ -13,17 +13,16 @@ export const tableDefinition: IArticleBlockDefinition = { ), hasUtilityWrapper: true, + hasDeleteBehaviorRemoveBlock: true, instanciateElement: () => SlateTable.createTable(2, 2), preventAutofocusTrigger: true, } export const tableRowDefinition: IArticleBlockDefinition = { name: "Table row", - icon: , + icon: , render: ({ attributes, element, children }) => { - return ( - {children} - ); + return {children} }, hasUtilityWrapper: false, } @@ -33,5 +32,5 @@ export const tableCellDefinition: IArticleBlockDefinition = { icon: , render: TableCellEditor, hasUtilityWrapper: false, - inlineMenu: ['strong', 'emphasis'], + inlineMenu: ["strong", "emphasis"], } diff --git a/src/components/NFTArticle/elements/Table/TableEditor.tsx b/src/components/NFTArticle/elements/Table/TableEditor.tsx index 9cb2ff2f1..ab2816788 100644 --- a/src/components/NFTArticle/elements/Table/TableEditor.tsx +++ b/src/components/NFTArticle/elements/Table/TableEditor.tsx @@ -1,9 +1,9 @@ -import React, { memo, MouseEventHandler, useCallback } from 'react'; +import React, { memo, MouseEventHandler, useCallback } from "react" import style from "./TableEditor.module.scss" -import { RenderElementProps, useSelected, useSlate } from "slate-react"; -import Slate from "slate"; -import { SlateTable } from "../../SlateEditor/Plugins/SlateTablePlugin"; -import { TableColToolbar } from "./TableColToolbar"; +import { RenderElementProps, useSelected, useSlate } from "slate-react" +import Slate from "slate" +import { SlateTable } from "../../SlateEditor/Plugins/SlateTablePlugin" +import { TableColToolbar } from "./TableColToolbar" import cs from "classnames" interface TableEditorProps { @@ -12,33 +12,42 @@ interface TableEditorProps { children: any } -const _TableEditor = ({ slateAttributes, slateElement, children }: TableEditorProps) => { - const editor = useSlate(); - const [head, ...body] = children; - const isSelected = useSelected(); - const handleClickAddCol = useCallback>((e) => { - e.preventDefault(); - SlateTable.addCol(editor, slateElement); - }, [editor, slateElement]) - const handleClickAddRow = useCallback>((e) => { - e.preventDefault(); - SlateTable.addRow(editor, slateElement) - }, [editor, slateElement]); - const selectedPos = isSelected && SlateTable.getSelectedPos(editor, slateElement); +const _TableEditor = ({ + slateAttributes, + slateElement, + children, +}: TableEditorProps) => { + const editor = useSlate() + const [head, ...body] = children + const isSelected = useSelected() + const handleClickAddCol = useCallback>( + (e) => { + e.preventDefault() + SlateTable.addCol(editor, slateElement) + }, + [editor, slateElement] + ) + const handleClickAddRow = useCallback>( + (e) => { + e.preventDefault() + SlateTable.addRow(editor, slateElement) + }, + [editor, slateElement] + ) + const selectedPos = + isSelected && SlateTable.getSelectedPos(editor, slateElement) return ( -
    +
    - - {head} - - - {body} - + {head} + {body}
    - {selectedPos && + {selectedPos && ( <> - } - {isSelected && + )} + {isSelected && ( <> - } + )}
    - ); -}; + ) +} -export const TableEditor = memo(_TableEditor); +export const TableEditor = memo(_TableEditor) diff --git a/src/components/NFTArticle/elements/TezosStorage/TezosStorageDisplay.module.scss b/src/components/NFTArticle/elements/TezosStorage/TezosStorageDisplay.module.scss index 4a369ecc2..58d5bb36e 100644 --- a/src/components/NFTArticle/elements/TezosStorage/TezosStorageDisplay.module.scss +++ b/src/components/NFTArticle/elements/TezosStorage/TezosStorageDisplay.module.scss @@ -1,5 +1,6 @@ .root { - width: min(800px, 100%); + width: 80vh; + max-width: min(800px, 100%); margin-left: auto; margin-right: auto; -} \ No newline at end of file +} diff --git a/src/components/NFTArticle/elements/TezosStorage/TezosStorageDisplay.tsx b/src/components/NFTArticle/elements/TezosStorage/TezosStorageDisplay.tsx index 380b21aa5..c69e2817d 100644 --- a/src/components/NFTArticle/elements/TezosStorage/TezosStorageDisplay.tsx +++ b/src/components/NFTArticle/elements/TezosStorage/TezosStorageDisplay.tsx @@ -1,11 +1,10 @@ -import React, { useMemo, FunctionComponent } from 'react' -import { ITezosStoragePointer } from '../../../../types/TezosStorage'; +import React, { useMemo, FunctionComponent } from "react" +import { ITezosStoragePointer } from "../../../../types/TezosStorage" import style from "./TezosStorageDisplay.module.scss" import cs from "classnames" -import { TezosStorageFactory } from './TezosStorageFactory' +import { TezosStorageFactory } from "./TezosStorageFactory" -export interface TezosStorageProps extends ITezosStoragePointer { -} +export interface TezosStorageProps extends ITezosStoragePointer {} export function TezosStorageDisplay({ contract, @@ -23,14 +22,12 @@ export function TezosStorageDisplay({ const Comp = TezosStorageFactory(pointer) const props = Comp.getPropsFromPointer(pointer) /* eslint-disable react/display-name */ - return () => ( - - ) + return () => }, [contract, path]) return (
    - +
    ) } diff --git a/src/components/NFTArticle/elements/TezosStorage/TezosStorageEditor.tsx b/src/components/NFTArticle/elements/TezosStorage/TezosStorageEditor.tsx index 53f4f8729..ed9a0f18b 100644 --- a/src/components/NFTArticle/elements/TezosStorage/TezosStorageEditor.tsx +++ b/src/components/NFTArticle/elements/TezosStorage/TezosStorageEditor.tsx @@ -9,81 +9,76 @@ import { Transforms } from "slate" import { BlockParamsModal } from "../../SlateEditor/UI/BlockParamsModal" import { TezosStorageDisplay } from "./TezosStorageDisplay" -export interface TezosStorageProps extends PropsWithChildren { +export interface TezosStorageProps + extends PropsWithChildren { element: any } -const TezosStorageEditor = forwardRef(({ - contract, - path, - storage_type, - data_spec, - value_path, - element, - children, -}, ref) => { - const editor = useSlateStatic() - const nodePath = ReactEditor.findPath(editor, element) +const TezosStorageEditor = forwardRef( + ( + { contract, path, storage_type, data_spec, value_path, element, children }, + ref + ) => { + const editor = useSlateStatic() + const nodePath = ReactEditor.findPath(editor, element) - const [showModal, setShowModal] = useState(false) + const [showModal, setShowModal] = useState(false) - // if there is no contract, the block isn't valid - const empty = !contract + // if there is no contract, the block isn't valid + const empty = !contract - // update the element with new values - const update = useCallback((element: any) => { - Transforms.setNodes(editor, element, { - at: nodePath - }) - }, [nodePath, element]) + // update the element with new values + const update = useCallback( + (element: any) => { + Transforms.setNodes(editor, element, { + at: nodePath, + }) + }, + [nodePath, element] + ) - return ( -
    -
    - {empty ? ( - - ):( - - )} -
    + return ( +
    +
    + {empty ? ( + + ) : ( + + )} +
    - {showModal && ( - setShowModal(false)} - > - { - update(element) - setShowModal(false) - }} - /> - - )} -
    - {children} + {showModal && ( + setShowModal(false)}> + { + update(element) + setShowModal(false) + }} + /> + + )} +
    {children}
    -
    - ) -}) + ) + } +) -TezosStorageEditor.displayName = 'TezosStorage' +TezosStorageEditor.displayName = "TezosStorage" export default TezosStorageEditor diff --git a/src/components/NFTArticle/elements/TezosStorage/TezosStorageFactory.ts b/src/components/NFTArticle/elements/TezosStorage/TezosStorageFactory.ts index 27b3b98a9..62afe2d2a 100644 --- a/src/components/NFTArticle/elements/TezosStorage/TezosStorageFactory.ts +++ b/src/components/NFTArticle/elements/TezosStorage/TezosStorageFactory.ts @@ -36,4 +36,4 @@ export function TezosStorageFactory( } } return TezosStorageUnknown -} \ No newline at end of file +} diff --git a/src/components/NFTArticle/elements/TezosStorage/TezosStorageGentk.module.scss b/src/components/NFTArticle/elements/TezosStorage/TezosStorageGentk.module.scss index a8ef8861f..1f182b94c 100644 --- a/src/components/NFTArticle/elements/TezosStorage/TezosStorageGentk.module.scss +++ b/src/components/NFTArticle/elements/TezosStorage/TezosStorageGentk.module.scss @@ -1,3 +1,9 @@ +.container { + display: flex; + flex-direction: column; + height: 100%; +} + .header { display: flex; flex-direction: row; @@ -13,4 +19,4 @@ .user { font-size: var(--font-size-small); } -} \ No newline at end of file +} diff --git a/src/components/NFTArticle/elements/TezosStorage/TezosStorageGentk.tsx b/src/components/NFTArticle/elements/TezosStorage/TezosStorageGentk.tsx index 954524adc..ffd7ac9e8 100644 --- a/src/components/NFTArticle/elements/TezosStorage/TezosStorageGentk.tsx +++ b/src/components/NFTArticle/elements/TezosStorage/TezosStorageGentk.tsx @@ -21,18 +21,16 @@ import { gentkLiveUrl } from "../../../../utils/objkt" interface Props { id: number } -export const TezosStorageGentk: TezosStorageRenderer = ({ - id, -}) => { +export const TezosStorageGentk: TezosStorageRenderer = ({ id }) => { const [running, setRunning] = useState(false) const { data } = useQuery(Qu_objkt, { variables: { - id: id - } + id: id, + }, }) - const token = useMemo(() => { + const token = useMemo(() => { return data?.objkt || null }, [data]) @@ -44,7 +42,7 @@ export const TezosStorageGentk: TezosStorageRenderer = ({ } return ( -
    +
    {token && (
    @@ -70,22 +68,16 @@ export const TezosStorageGentk: TezosStorageRenderer = ({ url={gentkLiveUrl(token)} hasLoading={false} /> - ):( - + ) : ( + ) - ):( - + ) : ( + loading token )} - {token && (
    {!running ? ( @@ -93,19 +85,19 @@ export const TezosStorageGentk: TezosStorageRenderer = ({ type="button" size="small" color="transparent" - iconComp={} + iconComp={} iconSide="right" onClick={() => setRunning(true)} > run - ):( + ) : ( <>
    )} - + {token && ( { TezosStorageProject.getPropsFromPointer = (pointer) => { return { - id: parseInt(pointer.path.split("::")[1]) + id: parseInt(pointer.path.split("::")[1]), } -} \ No newline at end of file +} diff --git a/src/components/NFTArticle/elements/TezosStorage/TezosStorageSettings.tsx b/src/components/NFTArticle/elements/TezosStorage/TezosStorageSettings.tsx index a6ecd106d..bae81296c 100644 --- a/src/components/NFTArticle/elements/TezosStorage/TezosStorageSettings.tsx +++ b/src/components/NFTArticle/elements/TezosStorage/TezosStorageSettings.tsx @@ -4,11 +4,17 @@ import cs from "classnames" import { Submit } from "../../../Form/Submit" import { Button } from "../../../Button" import { Field } from "../../../Form/Field" -import { InputReactiveSearch, InputReactSearchResultsRendererProps } from "../../../Input/InputReactiveSearch" +import { + InputReactiveSearch, + InputReactSearchResultsRendererProps, +} from "../../../Input/InputReactiveSearch" import { GenerativeToken } from "../../../../types/entities/GenerativeToken" import { Fragment, useCallback, useMemo, useState } from "react" import { useApolloClient, useQuery } from "@apollo/client" -import { Qu_genTokenAllIterations, Qu_searchGenTok } from "../../../../queries/generative-token" +import { + Qu_genTokenAllIterations, + Qu_searchGenTok, +} from "../../../../queries/generative-token" import { ipfsGatewayUrl } from "../../../../services/Ipfs" import { EntityBadge } from "../../../User/EntityBadge" import { Spacing } from "../../../Layout/Spacing" @@ -16,9 +22,11 @@ import { ModalTitle } from "../../SlateEditor/UI/ModalTitle" import { Objkt } from "../../../../types/entities/Objkt" import { LoaderBlock } from "../../../Layout/LoaderBlock" import { ImageIpfs } from "../../../Medias/ImageIpfs" -import { generativeTokenTezosStoragePointer, gentkTezosStoragePointer } from "../../../../utils/tezos-storage" -import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition"; - +import { + generativeTokenTezosStoragePointer, + gentkTezosStoragePointer, +} from "../../../../utils/tezos-storage" +import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition" export const TezosStorageSettings: TEditAttributeComp = ({ element, @@ -26,9 +34,9 @@ export const TezosStorageSettings: TEditAttributeComp = ({ children, }) => { // the selected project - const [project, setProject] = useState(null) + const [project, setProject] = useState(null) // the selected iteration - const [iteration, setIteration] = useState(null) + const [iteration, setIteration] = useState(null) // clears the selection -> state of iteration & project const clearSelection = useCallback(() => { @@ -39,73 +47,66 @@ export const TezosStorageSettings: TEditAttributeComp = ({ const importSelection = useCallback(() => { if (iteration) { onEdit(gentkTezosStoragePointer(iteration)) - } - else if (project) { + } else if (project) { onEdit(generativeTokenTezosStoragePointer(project)) } }, [onEdit, project, iteration]) return (
    - - Insert fxhash content - + Insert fxhash content

    - You can first select a project, and then insert the project itself or pick a particular iteration of the project. + You can first select a project, and then insert the project itself or + pick a particular iteration of the project.

    - + -
    - +
    +
    {project && ( -
    +
    Selected project -
    - +
    )} {project && ( <> - -
    + +
    {iteration ? "Iteration selected" - : "You can select an iteration" - } + : "You can select an iteration"} {iteration && ( - )}
    @@ -145,11 +146,9 @@ function ProjectsReactiveSearchResultsRenderer({ return (
    {results?.map((token) => ( - + {children?.({ - item: token + item: token, })} ))} @@ -157,7 +156,6 @@ function ProjectsReactiveSearchResultsRenderer({ ) } - /** * The component to quickly search and explore projects, with a list to display * the projects. @@ -165,9 +163,7 @@ function ProjectsReactiveSearchResultsRenderer({ interface IImportCompProps { onChange: (project: GenerativeToken) => void } -function TezosStorageLoadProject({ - onChange, -}: IImportCompProps) { +function TezosStorageLoadProject({ onChange }: IImportCompProps) { const client = useApolloClient() const [value, setValue] = useState("") @@ -182,21 +178,21 @@ function TezosStorageLoadProject({ searchQuery_eq: search, }, sort: { - relevance: "DESC" - } - } + relevance: "DESC", + }, + }, }) } const resultsIntoGenToks = (results: any): GenerativeToken[] => { - if (!results || ! results.data || !results.data.generativeTokens) { + if (!results || !results.data || !results.data.generativeTokens) { return [] } return results.data.generativeTokens } const valueFromToken = (token: GenerativeToken) => { - return ""+token.id + return "" + token.id } // update the value and clear the selection @@ -225,9 +221,7 @@ function TezosStorageLoadProject({ onChange(item) }} > - + )} @@ -236,16 +230,13 @@ function TezosStorageLoadProject({ ) } - /** * A simple component to render a project with a one-liner */ interface IProjectRendererOneLineProps { project: GenerativeToken } -function ProjectRendererOneLine({ - project, -}: IProjectRendererOneLineProps) { +function ProjectRendererOneLine({ project }: IProjectRendererOneLineProps) { return (
    #{project.id} {project.name}
    - +
    ) } - /*** * Component to select an iteration from a project */ interface IProjectIterationPickerProps { project: GenerativeToken - selected: Objkt|null - onChange: (value: Objkt|null) => void + selected: Objkt | null + onChange: (value: Objkt | null) => void } function ProjectIterationPicker({ project, @@ -283,7 +270,7 @@ function ProjectIterationPicker({ const { data } = useQuery(Qu_genTokenAllIterations, { variables: { id: project.id, - } + }, }) const iterations = useMemo(() => { @@ -307,25 +294,20 @@ function ProjectIterationPicker({ key={item.id} type="button" className={cs(style.iteration, { - [style.selected]: selected?.id === item.id + [style.selected]: selected?.id === item.id, })} onClick={() => onChange(selected?.id === item.id ? null : item)} > -
    +
    - +
    #{item.iteration} ))}
    - ):( - + ) : ( + )}
    ) diff --git a/src/components/NFTArticle/elements/TezosStorage/TezosStorageUnknown.tsx b/src/components/NFTArticle/elements/TezosStorage/TezosStorageUnknown.tsx index ba2f8be44..c2dcc4a7d 100644 --- a/src/components/NFTArticle/elements/TezosStorage/TezosStorageUnknown.tsx +++ b/src/components/NFTArticle/elements/TezosStorage/TezosStorageUnknown.tsx @@ -22,7 +22,7 @@ export const TezosStorageUnknown: TezosStorageRenderer = ({ return (
    - + unsupported tezos storage content
    {keys.map((key) => ( @@ -38,4 +38,4 @@ export const TezosStorageUnknown: TezosStorageRenderer = ({ // fallback, always matches TezosStorageUnknown.matches = () => true // no props -TezosStorageUnknown.getPropsFromPointer = pointer => ({ pointer }) \ No newline at end of file +TezosStorageUnknown.getPropsFromPointer = (pointer) => ({ pointer }) diff --git a/src/components/NFTArticle/elements/ThematicBreak/ThematicBreakDefinition.tsx b/src/components/NFTArticle/elements/ThematicBreak/ThematicBreakDefinition.tsx index 8bbc4ecf6..eeec0495e 100644 --- a/src/components/NFTArticle/elements/ThematicBreak/ThematicBreakDefinition.tsx +++ b/src/components/NFTArticle/elements/ThematicBreak/ThematicBreakDefinition.tsx @@ -1,5 +1,5 @@ -import { ThematicBreakEditor } from "./ThematicBreakEditor"; -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; +import { ThematicBreakEditor } from "./ThematicBreakEditor" +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" export const thematicBreakDefinition: IArticleBlockDefinition = { name: "Horizontal break", @@ -7,9 +7,11 @@ export const thematicBreakDefinition: IArticleBlockDefinition = { render: ThematicBreakEditor, instanciateElement: () => ({ type: "thematicBreak", - children: [{ - text: "" - }], + children: [ + { + text: "", + }, + ], }), buttonInstantiable: true, hasUtilityWrapper: true, diff --git a/src/components/NFTArticle/elements/ThematicBreak/ThematicBreakEditor.tsx b/src/components/NFTArticle/elements/ThematicBreak/ThematicBreakEditor.tsx index be2b5b87d..9d5cc0324 100644 --- a/src/components/NFTArticle/elements/ThematicBreak/ThematicBreakEditor.tsx +++ b/src/components/NFTArticle/elements/ThematicBreak/ThematicBreakEditor.tsx @@ -13,11 +13,15 @@ export function ThematicBreakEditor({ children, }: PropsWithChildren) { return ( -
    - {children &&
    - {children} -
    } -
    +
    + {children && ( +
    {children}
    + )} +
    ) } diff --git a/src/components/NFTArticle/elements/Video/VideoAttributeSettings.tsx b/src/components/NFTArticle/elements/Video/VideoAttributeSettings.tsx index 2cf299d4c..6f14ceaf3 100644 --- a/src/components/NFTArticle/elements/Video/VideoAttributeSettings.tsx +++ b/src/components/NFTArticle/elements/Video/VideoAttributeSettings.tsx @@ -3,44 +3,51 @@ import style from "./VideoAttributeSettings.module.scss" import { Dropzone } from "../../../Input/Dropzone" import { TabDefinition } from "../../../Layout/Tabs" import { TabsContainer } from "../../../Layout/TabsContainer" -import { useCallback, useState } from "react"; -import { Field } from "../../../Form/Field"; -import { InputText } from "../../../Input/InputText"; -import { Submit } from "../../../Form/Submit"; -import { Button } from "../../../Button"; -import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition"; +import { useCallback, useState } from "react" +import { Field } from "../../../Form/Field" +import { InputText } from "../../../Input/InputText" +import { Submit } from "../../../Form/Submit" +import { Button } from "../../../Button" +import { TEditAttributeComp } from "../../../../types/ArticleEditor/BlockDefinition" const tabs: TabDefinition[] = [ { - name: "Upload" + name: "Upload", }, { - name: "External link" - } + name: "External link", + }, ] -const acceptedVideoFiles = ["video/mp4", "video/ogg", "video/webm"]; +const acceptedVideoFiles = ["video/mp4", "video/ogg", "video/webm"] -export const VideoAttributeSettings: TEditAttributeComp = ({ - onEdit, -}) => { +export const VideoAttributeSettings: TEditAttributeComp = ({ onEdit }) => { const [textUrl, setTextUrl] = useState("") - const handleAddFile = useCallback((files) => { - if (!files || !files[0]) return null; - const [file] = files; - if (file) { + const handleAddFile = useCallback( + (files) => { + if (!files || !files[0]) return null + const [file] = files + if (file) { + onEdit({ + src: URL.createObjectURL(file), + }) + } + }, + [onEdit] + ) + const handleChangeUrlVideo = useCallback( + (event) => setTextUrl(event.target.value), + [] + ) + const handleImportVideoFromUrl = useCallback( + (event) => { + event.preventDefault() onEdit({ - src: URL.createObjectURL(file) + src: textUrl, }) - } - }, [onEdit]) - const handleChangeUrlVideo = useCallback(event => setTextUrl(event.target.value), []); - const handleImportVideoFromUrl = useCallback((event) => { - event.preventDefault() - onEdit({ - src: textUrl - }) - }, [onEdit, textUrl]) + }, + [onEdit, textUrl] + ) return (
    @@ -56,27 +63,20 @@ export const VideoAttributeSettings: TEditAttributeComp = ({ onChange={handleAddFile} accepted={acceptedVideoFiles} className={cs(style.dropzone)} - textDefault={( + textDefault={
    - - - Import a video (100mb max) - + + Import a video (50mb max)
    - )} - textDrag={( + } + textDrag={
    - - Drop video file - + Drop video file
    - )} + } /> - ) - : tabIndex === 1 ? ( -
    + ) : tabIndex === 1 ? ( +
    - ): null} + ) : null}
    )} diff --git a/src/components/NFTArticle/elements/Video/VideoDefinition.tsx b/src/components/NFTArticle/elements/Video/VideoDefinition.tsx index c20c9eb8f..8e5211e38 100644 --- a/src/components/NFTArticle/elements/Video/VideoDefinition.tsx +++ b/src/components/NFTArticle/elements/Video/VideoDefinition.tsx @@ -1,5 +1,5 @@ -import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition"; -import { VideoEditor } from "./VideoEditor"; +import { IArticleBlockDefinition } from "../../../../types/ArticleEditor/BlockDefinition" +import { VideoEditor } from "./VideoEditor" interface InstanciateVideoOpts { src?: string @@ -7,7 +7,7 @@ interface InstanciateVideoOpts { } export const videoDefinition: IArticleBlockDefinition = { name: "Video", - icon: , + icon: , buttonInstantiable: true, render: ({ attributes, element, children }) => { return ( @@ -17,19 +17,26 @@ export const videoDefinition: IArticleBlockDefinition = { ) }, hasUtilityWrapper: false, - instanciateElement: (opts = { src: '', caption: '' }) => ({ + instanciateElement: (opts = { src: "", caption: "" }) => ({ type: "figure", - children: [{ - type: "video", - src: opts.src, - children: [{ - text: "" - }] - }, { - type: "figcaption", - children: [{ - text: opts.caption - }] - }] + children: [ + { + type: "video", + src: opts.src, + children: [ + { + text: "", + }, + ], + }, + { + type: "figcaption", + children: [ + { + text: opts.caption, + }, + ], + }, + ], }), -}; +} diff --git a/src/components/NFTArticle/elements/Video/VideoDisplay.tsx b/src/components/NFTArticle/elements/Video/VideoDisplay.tsx index 3c1fdd60a..2d3e39599 100644 --- a/src/components/NFTArticle/elements/Video/VideoDisplay.tsx +++ b/src/components/NFTArticle/elements/Video/VideoDisplay.tsx @@ -1,5 +1,5 @@ -import React, { memo } from 'react'; -import { VideoPolymorphic } from "../../../Medias/VideoPolymorphic"; +import React, { memo } from "react" +import { VideoPolymorphic } from "../../../Medias/VideoPolymorphic" interface VideoDisplayProps { src: string @@ -8,16 +8,9 @@ interface VideoDisplayProps { const _VideoDisplay = ({ src, children }: VideoDisplayProps) => (
    - - {src && ( -
    - {children} -
    - )} + + {src &&
    {children}
    }
    -); +) -export const VideoDisplay = memo(_VideoDisplay); +export const VideoDisplay = memo(_VideoDisplay) diff --git a/src/components/NFTArticle/elements/Video/VideoEditor.tsx b/src/components/NFTArticle/elements/Video/VideoEditor.tsx index ec93a210b..08b52d846 100644 --- a/src/components/NFTArticle/elements/Video/VideoEditor.tsx +++ b/src/components/NFTArticle/elements/Video/VideoEditor.tsx @@ -1,23 +1,27 @@ -import { memo, NamedExoticComponent, PropsWithChildren, useCallback, useState } from "react" +import { + memo, + NamedExoticComponent, + PropsWithChildren, + useCallback, + useState, +} from "react" import { Transforms } from "slate" import { ReactEditor, useSlateStatic } from "slate-react" import style from "./VideoEditor.module.scss" import editorStyle from "../../SlateEditor/UI/EditorStyles.module.scss" import cs from "classnames" -import { BlockParamsModal } from "../../SlateEditor/UI/BlockParamsModal"; -import { VideoAttributeSettings } from "./VideoAttributeSettings"; -import { VideoPolymorphic } from "../../../Medias/VideoPolymorphic"; +import { BlockParamsModal } from "../../SlateEditor/UI/BlockParamsModal" +import { VideoAttributeSettings } from "./VideoAttributeSettings" +import { VideoPolymorphic } from "../../../Medias/VideoPolymorphic" interface VideoElementProps { attributes?: any element?: any - src?: string, + src?: string } -export const VideoEditor: NamedExoticComponent> = memo(({ - attributes, - element, - children, -}) => { +export const VideoEditor: NamedExoticComponent< + PropsWithChildren +> = memo(({ attributes, element, children }) => { const [showAddVideo, setShowAddVideo] = useState(false) const editor = useSlateStatic() const path = ReactEditor.findPath(editor, element) @@ -25,15 +29,22 @@ export const VideoEditor: NamedExoticComponent { event.preventDefault() event.stopPropagation() - setShowAddVideo(true); + setShowAddVideo(true) }, []) - const handleCloseAddVideo = useCallback(() => setShowAddVideo(false), []); - const handleAddVideo = useCallback((element) => { - Transforms.setNodes(editor, { src: element.src }, { - at: path - }) - setShowAddVideo(false); - }, [editor, path]) + const handleCloseAddVideo = useCallback(() => setShowAddVideo(false), []) + const handleAddVideo = useCallback( + (element) => { + Transforms.setNodes( + editor, + { src: element.src }, + { + at: path, + } + ) + setShowAddVideo(false) + }, + [editor, path] + ) const hasUrl = element.src !== "" @@ -47,33 +58,27 @@ export const VideoEditor: NamedExoticComponent - ):( + ) : ( )}
    {showAddVideo && ( - - + + )} ) -}); +}) -VideoEditor.displayName = 'VideoElement'; +VideoEditor.displayName = "VideoElement" diff --git a/src/components/NFTArticle/elements/Video/VideoProcessor.ts b/src/components/NFTArticle/elements/Video/VideoProcessor.ts index 1027fb966..8ea1ffe1f 100644 --- a/src/components/NFTArticle/elements/Video/VideoProcessor.ts +++ b/src/components/NFTArticle/elements/Video/VideoProcessor.ts @@ -1,24 +1,29 @@ -import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor"; +import { IArticleElementProcessor } from "../../../../types/ArticleEditor/Processor" export const videoProcessor: IArticleElementProcessor = { transformMdhastToComponent: (node, properties) => { - return ({ - src: properties.src || '', - }) + return { + src: properties.src || "", + } }, transformMarkdownMdhastToSlate: (node) => { - return ({ + return { type: "figure", - children: [{ - type: "video", - src: node.src || '', - children: [{ - text: "" - }], - }, { - type: "figcaption", - children: node.children, - }] - }); - } + children: [ + { + type: "video", + src: node.src || "", + children: [ + { + text: "", + }, + ], + }, + { + type: "figcaption", + children: node.children, + }, + ], + } + }, } diff --git a/src/components/NFTArticle/processor/getMarkdownFromSlateEditorState.ts b/src/components/NFTArticle/processor/getMarkdownFromSlateEditorState.ts index a6703214e..57833265d 100644 --- a/src/components/NFTArticle/processor/getMarkdownFromSlateEditorState.ts +++ b/src/components/NFTArticle/processor/getMarkdownFromSlateEditorState.ts @@ -1,43 +1,42 @@ -import { Node } from "slate"; -import { unified } from "unified"; -import remarkMath from "remark-math"; -import remarkDirective from "remark-directive"; -import remarkUnwrapImages from "remark-unwrap-images"; -import { slateToRemark } from "remark-slate-transformer"; -import stringify from "remark-stringify"; -import { OverridedSlateBuilders } from "remark-slate-transformer/lib/transformers/slate-to-mdast"; -import { remarkFxHashCustom } from "./plugins"; -import remarkGfm from "remark-gfm"; -import { mathProcessor } from "../elements/Math/MathProcessor"; -import { figureProcessor } from "../elements/Figure/FigureProcessor"; +import { Node } from "slate" +import { unified } from "unified" +import remarkMath from "remark-math" +import remarkDirective from "remark-directive" +import remarkUnwrapImages from "remark-unwrap-images" +import { slateToRemark } from "remark-slate-transformer" +import stringify from "remark-stringify" +import { OverridedSlateBuilders } from "remark-slate-transformer/lib/transformers/slate-to-mdast" +import { remarkFxHashCustom } from "./plugins" +import remarkGfm from "remark-gfm" +import { mathProcessor } from "../elements/Math/MathProcessor" +import { figureProcessor } from "../elements/Figure/FigureProcessor" +import { mentionProcessor } from "../elements/Mention/MentionProcessor" -export function convertSlateLeafDirectiveToMarkdown( - node: any, -) { - const { children, type, ...attributes} = node +export function convertSlateLeafDirectiveToMarkdown(node: any) { + const { children, type, ...attributes } = node return { - type: 'leafDirective', + type: "leafDirective", name: type, children: [ { - type: 'text', + type: "text", value: children[0].text, - } + }, ], attributes, } } - const slateToRemarkTransformerOverrides: OverridedSlateBuilders = { - 'tezos-storage-pointer': convertSlateLeafDirectiveToMarkdown, - 'embed-media': convertSlateLeafDirectiveToMarkdown, + "tezos-storage-pointer": convertSlateLeafDirectiveToMarkdown, + "embed-media": convertSlateLeafDirectiveToMarkdown, figure: figureProcessor.transformSlateToMarkdownMdhast!, inlineMath: mathProcessor.transformSlateToMarkdownMdhast!, math: mathProcessor.transformSlateToMarkdownMdhast!, + mention: mentionProcessor.transformSlateToMarkdownMdhast!, } -export default async function getMarkdownFromSlateEditorState(slate: Node[] ) { +export default async function getMarkdownFromSlateEditorState(slate: Node[]) { try { const processor = unified() .use(remarkMath) @@ -48,7 +47,7 @@ export default async function getMarkdownFromSlateEditorState(slate: Node[] ) { .use(slateToRemark, { overrides: slateToRemarkTransformerOverrides, }) - .use(stringify, { bulletOther: '-' }) + .use(stringify, { bulletOther: "-" }) const ast = await processor.run({ type: "root", children: slate, @@ -80,17 +79,15 @@ export default async function getMarkdownFromSlateEditorState(slate: Node[] ) { } ) return `::tezos-storage-pointer[${alt}]{${replaced}}` - } - else { + } else { return match } } ) return directiveAttributesFixed - } - catch(e) { + } catch (e) { console.error(e) - return null; + return null } } diff --git a/src/components/NFTArticle/processor/getNFTArticleComponentsFromMarkdown.ts b/src/components/NFTArticle/processor/getNFTArticleComponentsFromMarkdown.ts index 6737de1ec..7306328c1 100644 --- a/src/components/NFTArticle/processor/getNFTArticleComponentsFromMarkdown.ts +++ b/src/components/NFTArticle/processor/getNFTArticleComponentsFromMarkdown.ts @@ -1,27 +1,36 @@ -import { createElement, Fragment } from "react"; -import Embed from "../elements/Embed/Embed"; -import matter from "gray-matter"; -import { unified } from "unified"; -import remarkParse from "remark-parse"; -import remarkMath from "remark-math"; -import remarkGfm from "remark-gfm"; -import remarkUnwrapImages from "remark-unwrap-images"; -import remarkDirective from "remark-directive"; -import remarkRehype from "remark-rehype"; -import rehypeKatex from "rehype-katex"; +import { createElement, Fragment } from "react" +import Embed from "../elements/Embed/Embed" +import matter from "gray-matter" +import { unified } from "unified" +import remarkParse from "remark-parse" +import remarkMath from "remark-math" +import remarkGfm from "remark-gfm" +import remarkUnwrapImages from "remark-unwrap-images" +import remarkDirective from "remark-directive" +import remarkRehype from "remark-rehype" +import rehypeKatex from "rehype-katex" import rehypePrism from "rehype-prism" -import rehypeFormat from "rehype-format"; -import rehypeStringify from "rehype-stringify"; -import rehypeReact from "rehype-react"; -import { Element } from "hast"; -import { ComponentsWithNodeOptions, ComponentsWithoutNodeOptions } from "rehype-react/lib/complex-types"; -import { SharedOptions } from "rehype-react/lib"; -import { mdastFlattenListItemParagraphs, remarkFxHashCustom } from "./plugins" +import rehypeStringify from "rehype-stringify" +import rehypeReact from "rehype-react" +import { Element } from "hast" +import { + ComponentsWithNodeOptions, + ComponentsWithoutNodeOptions, +} from "rehype-react/lib/complex-types" +import { SharedOptions } from "rehype-react/lib" +import { + mdastFlattenListItemParagraphs, + mdastParseMentions, + remarkFxHashCustom, + remarkMentions, +} from "./plugins" import { TezosStorageDisplay } from "../elements/TezosStorage/TezosStorageDisplay" -import { ImageDisplay } from "../elements/Image/ImageDisplay"; -import { CodeDisplay } from "../elements/Code/CodeDisplay"; -import { ThematicBreakEditor } from "../elements/ThematicBreak/ThematicBreakEditor"; -import { VideoDisplay } from "../elements/Video/VideoDisplay"; +import { ImageDisplay } from "../elements/Image/ImageDisplay" +import { CodeDisplay } from "../elements/Code/CodeDisplay" +import { ThematicBreakEditor } from "../elements/ThematicBreak/ThematicBreakEditor" +import { VideoDisplay } from "../elements/Video/VideoDisplay" +import { LinkElement } from "../elements/Link/LinkElement" +import { MentionDisplay } from "../elements/Mention/MentionDisplay" declare module "rehype-react" { interface WithNode { @@ -30,7 +39,8 @@ declare module "rehype-react" { interface CustomComponentsOptions { [key: string]: any } - interface CustomComponentsWithoutNodeOptions extends Omit { + interface CustomComponentsWithoutNodeOptions + extends Omit { components?: CustomComponentsOptions } export type Options = SharedOptions & @@ -38,40 +48,45 @@ declare module "rehype-react" { | ComponentsWithNodeOptions | ComponentsWithoutNodeOptions | CustomComponentsWithoutNodeOptions - ); + ) } const settingsRehypeReact = { createElement, Fragment, components: { - 'tezos-storage-pointer': TezosStorageDisplay, - 'embed-media': Embed, - 'img': ImageDisplay, - 'video': VideoDisplay, - 'pre': CodeDisplay, - 'hr': ThematicBreakEditor, - } + "tezos-storage-pointer": TezosStorageDisplay, + "embed-media": Embed, + img: ImageDisplay, + video: VideoDisplay, + pre: CodeDisplay, + hr: ThematicBreakEditor, + a: LinkElement, + mention: MentionDisplay, + }, } interface PayloadNFTArticleComponentsFromMarkdown { [p: string]: any content: any } -export default async function getNFTArticleComponentsFromMarkdown(markdown: string): Promise { +export default async function getNFTArticleComponentsFromMarkdown( + markdown: string +): Promise { try { const matterResult = matter(markdown) const processed = await unified() .use(remarkParse) .use(mdastFlattenListItemParagraphs) + .use(mdastParseMentions) .use(remarkMath) .use(remarkGfm) .use(remarkUnwrapImages) .use(remarkDirective) .use(remarkFxHashCustom) + .use(remarkMentions) .use(remarkRehype) .use(rehypePrism) .use(rehypeKatex) - .use(rehypeFormat) .use(rehypeStringify) // todo: fix this, because of image component for some reason // @ts-ignore @@ -82,9 +97,7 @@ export default async function getNFTArticleComponentsFromMarkdown(markdown: stri ...matterResult.data, content: processed.result, } - } - catch { + } catch { return null } } - diff --git a/src/components/NFTArticle/processor/getSlateEditorStateFromMarkdown.ts b/src/components/NFTArticle/processor/getSlateEditorStateFromMarkdown.ts index cf07d94ea..7cc1aa283 100644 --- a/src/components/NFTArticle/processor/getSlateEditorStateFromMarkdown.ts +++ b/src/components/NFTArticle/processor/getSlateEditorStateFromMarkdown.ts @@ -1,51 +1,65 @@ -import { Descendant } from "slate"; -import matter from "gray-matter"; -import { unified } from "unified"; -import remarkParse from "remark-parse"; -import remarkMath from "remark-math"; -import remarkUnwrapImages from "remark-unwrap-images"; -import remarkDirective from "remark-directive"; -import { remarkToSlate } from "remark-slate-transformer"; -import { OverridedMdastBuilders } from "remark-slate-transformer/lib/transformers/mdast-to-slate"; -import { mdastFlattenListItemParagraphs, remarkFxHashCustom } from "./plugins"; -import remarkGfm from "remark-gfm"; -import { mathProcessor } from "../elements/Math/MathProcessor"; -import { imageProcessor } from "../elements/Image/ImageProcessor"; -import { videoProcessor } from "../elements/Video/VideoProcessor"; +import { Descendant } from "slate" +import matter from "gray-matter" +import { unified } from "unified" +import remarkParse from "remark-parse" +import remarkMath from "remark-math" +import remarkUnwrapImages from "remark-unwrap-images" +import remarkDirective from "remark-directive" +import { remarkToSlate } from "remark-slate-transformer" +import { OverridedMdastBuilders } from "remark-slate-transformer/lib/transformers/mdast-to-slate" +import { + mdastFlattenListItemParagraphs, + mdastParseMentions, + remarkFxHashCustom, +} from "./plugins" +import remarkGfm from "remark-gfm" +import { mathProcessor } from "../elements/Math/MathProcessor" +import { imageProcessor } from "../elements/Image/ImageProcessor" +import { videoProcessor } from "../elements/Video/VideoProcessor" +import { mentionProcessor } from "../elements/Mention/MentionProcessor" -interface DirectiveNodeProps { [key: string]: any } +interface DirectiveNodeProps { + [key: string]: any +} const directives: Record object> = { - "video": videoProcessor.transformMarkdownMdhastToSlate!, + video: videoProcessor.transformMarkdownMdhastToSlate!, } -function createDirectiveNode(node: any, next: (children: any[]) => any): object { +function createDirectiveNode( + node: any, + next: (children: any[]) => any +): object { const data = node.data || {} - const hProperties: {[key:string]: any} = (data.hProperties || {}) as {[key:string]: any} + const hProperties: { [key: string]: any } = (data.hProperties || {}) as { + [key: string]: any + } // extract only defined props to avoid error serialization of undefined - const propertiesWithoutUndefined: DirectiveNodeProps = Object.keys(hProperties) - .reduce((acc: DirectiveNodeProps, key: string) =>{ - const value = hProperties[key]; - if (value) { - acc[key] = value; - } - return acc; - }, {}); + const propertiesWithoutUndefined: DirectiveNodeProps = Object.keys( + hProperties + ).reduce((acc: DirectiveNodeProps, key: string) => { + const value = hProperties[key] + if (value) { + acc[key] = value + } + return acc + }, {}) const newNode = { type: data.hName, children: next(node.children), - ...propertiesWithoutUndefined - }; + ...propertiesWithoutUndefined, + } const instanciateNode = directives[newNode.type] - return instanciateNode ? instanciateNode(newNode) : newNode; + return instanciateNode ? instanciateNode(newNode) : newNode } const remarkSlateTransformerOverrides: OverridedMdastBuilders = { textDirective: createDirectiveNode, leafDirective: createDirectiveNode, containerDirective: createDirectiveNode, - "inlineMath": mathProcessor.transformMarkdownMdhastToSlate, - "math": mathProcessor.transformMarkdownMdhastToSlate, + inlineMath: mathProcessor.transformMarkdownMdhastToSlate, + math: mathProcessor.transformMarkdownMdhastToSlate, image: imageProcessor.transformMarkdownMdhastToSlate, + mention: mentionProcessor.transformMarkdownMdhastToSlate, } interface PayloadSlateEditorStateFromMarkdown { @@ -53,28 +67,31 @@ interface PayloadSlateEditorStateFromMarkdown { editorState: Descendant[] } -export default async function getSlateEditorStateFromMarkdown(markdown: string): Promise { +export default async function getSlateEditorStateFromMarkdown( + markdown: string +): Promise { try { const matterResult = matter(markdown) const processed = await unified() .use(remarkParse) .use(mdastFlattenListItemParagraphs) + .use(mdastParseMentions) .use(remarkMath) .use(remarkGfm) .use(remarkUnwrapImages) .use(remarkDirective) .use(remarkFxHashCustom) .use(remarkToSlate, { - overrides: remarkSlateTransformerOverrides + overrides: remarkSlateTransformerOverrides, }) .process(matterResult.content) return { ...matterResult.data, - editorState: processed.result as Descendant[] - }; - } catch(e) { + editorState: processed.result as Descendant[], + } + } catch (e) { console.error(e) - return null; + return null } } diff --git a/src/components/NFTArticle/processor/index.ts b/src/components/NFTArticle/processor/index.ts index c2c7940a3..8293a9a15 100644 --- a/src/components/NFTArticle/processor/index.ts +++ b/src/components/NFTArticle/processor/index.ts @@ -1,3 +1,3 @@ -export { default as getMarkdownFromSlateEditorState } from "./getMarkdownFromSlateEditorState"; -export { default as getSlateEditorStateFromMarkdown } from "./getSlateEditorStateFromMarkdown"; -export { default as getNFTArticleComponentsFromMarkdown } from "./getNFTArticleComponentsFromMarkdown"; +export { default as getMarkdownFromSlateEditorState } from "./getMarkdownFromSlateEditorState" +export { default as getSlateEditorStateFromMarkdown } from "./getSlateEditorStateFromMarkdown" +export { default as getNFTArticleComponentsFromMarkdown } from "./getNFTArticleComponentsFromMarkdown" diff --git a/src/components/NFTArticle/processor/plugins.ts b/src/components/NFTArticle/processor/plugins.ts index 3b31be66a..431301dda 100644 --- a/src/components/NFTArticle/processor/plugins.ts +++ b/src/components/NFTArticle/processor/plugins.ts @@ -1,22 +1,36 @@ -import { Root } from "mdast" +import { Parent, PhrasingContent, Root } from "mdast" import { Transformer } from "unified" import { visit } from "unist-util-visit" import { h } from "hastscript" -import { IArticleElementProcessor } from "../../../types/ArticleEditor/Processor"; -import { embedProcessor } from "../elements/Embed/EmbedProcessor"; -import { tezosStorageProcessor } from "../elements/TezosStorage/TezosStorageProcessor"; -import { videoProcessor } from "../elements/Video/VideoProcessor"; +import { u } from "unist-builder" +import { IArticleElementProcessor } from "../../../types/ArticleEditor/Processor" +import { embedProcessor } from "../elements/Embed/EmbedProcessor" +import { tezosStorageProcessor } from "../elements/TezosStorage/TezosStorageProcessor" +import { videoProcessor } from "../elements/Video/VideoProcessor" +import { findAndReplace } from "mdast-util-find-and-replace" +import { mentionProcessor } from "../elements/Mention/MentionProcessor" + +// declare module 'mdast' { +// interface Mention extends Parent { +// type: 'mention'; +// value: string; +// children: PhrasingContent[]; +// } +// interface StaticPhrasingContentMap { +// mention: Mention; +// } +// } interface CustomArticleElementsByType { - leafDirective: Record, - textDirective: Record, - containerDirective: Record, + leafDirective: Record + textDirective: Record + containerDirective: Record } export const customNodes: CustomArticleElementsByType = { leafDirective: { "tezos-storage-pointer": tezosStorageProcessor, "embed-media": embedProcessor, - "video": videoProcessor, + video: videoProcessor, }, textDirective: {}, containerDirective: {}, @@ -25,18 +39,42 @@ export function remarkFxHashCustom(): Transformer { return (tree: Root) => { visit(tree, (node) => { if ( - node.type === 'textDirective' || - node.type === 'leafDirective' || - node.type === 'containerDirective' + node.type === "textDirective" || + node.type === "leafDirective" || + node.type === "containerDirective" ) { const component = customNodes[node.type]?.[node.name] if (component?.transformMdhastToComponent) { const hast: any = h(node.name, node.attributes) - const props = component.transformMdhastToComponent(node, hast.properties) + const props = component.transformMdhastToComponent( + node, + hast.properties + ) if (props) { const data = node.data || (node.data = {}) data.hName = component.htmlTagName || hast.tagName - data.hProperties = props; + data.hProperties = props + } + } + } + }) + } +} +export function remarkMentions(): Transformer { + return (tree: Root) => { + visit(tree, (node: any) => { + if (node.type === "mention") { + const component = mentionProcessor + if (component?.transformMdhastToComponent) { + const hast: any = h(node.name, { value: node.value }) + const props = component.transformMdhastToComponent( + node, + hast.properties + ) + if (props) { + const data = node.data || (node.data = {}) + data.hName = component.htmlTagName || hast.tagName + data.hProperties = props } } } @@ -46,16 +84,32 @@ export function remarkFxHashCustom(): Transformer { export function mdastFlattenListItemParagraphs(): Transformer { return (ast) => { - visit(ast, 'listItem', (listItem: any) => { + visit(ast, "listItem", (listItem: any) => { if ( listItem.children.length === 1 && - listItem.children[0].type === 'paragraph' + listItem.children[0].type === "paragraph" ) { - listItem.children = listItem.children[0].children; + listItem.children = listItem.children[0].children } - return listItem; - }); - return ast; - }; + return listItem + }) + return ast + } } +export function mdastParseMentions(): Transformer { + return (ast) => { + // @ts-ignore + findAndReplace(ast, [ + [ + /@(tz[1-3][1-9a-zA-Z]{33})/g, + function ($0: any, $1: any) { + return u("mention", { name: "mention", value: $1 }, [ + { type: "text", value: "" }, + ]) + }, + ], + ]) + return ast + } +} diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 86f86ab76..943bc3519 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -1,17 +1,17 @@ -import Link from 'next/link' -import style from './Navigation.module.scss' -import text from '../styles/Text.module.css' -import effects from '../styles/Effects.module.scss' -import cs from 'classnames' -import { Button } from './Button' -import { useContext, useMemo } from 'react' -import { UserContext } from '../containers/UserProvider' -import { Dropdown } from './Navigation/Dropdown' -import { Avatar } from './User/Avatar' -import { getUserProfileLink } from '../utils/user' -import { useState, useEffect } from 'react' -import { useRouter } from 'next/router' -import { SettingsModal } from '../containers/Settings/SettingsModal' +import Link from "next/link" +import style from "./Navigation.module.scss" +import text from "../styles/Text.module.css" +import effects from "../styles/Effects.module.scss" +import cs from "classnames" +import { Button } from "./Button" +import { useContext, useMemo } from "react" +import { UserContext } from "../containers/UserProvider" +import { Dropdown } from "./Navigation/Dropdown" +import { Avatar } from "./User/Avatar" +import { getUserProfileLink } from "../utils/user" +import { useState, useEffect } from "react" +import { useRouter } from "next/router" +import { SettingsModal } from "../containers/Settings/SettingsModal" export function Navigation() { const userCtx = useContext(UserContext) @@ -30,13 +30,20 @@ export function Navigation() { return ( <> {settingsModal && ( - setSettingsModal(false)} - /> + setSettingsModal(false)} /> )} ) diff --git a/src/components/Navigation/Dropdown.module.scss b/src/components/Navigation/Dropdown.module.scss index 5addbeec0..d6c98be85 100644 --- a/src/components/Navigation/Dropdown.module.scss +++ b/src/components/Navigation/Dropdown.module.scss @@ -53,31 +53,33 @@ } @media (max-width: $breakpoint-md) { - .container { - display: flex; - flex-direction: column; - align-items: flex-end; - } + .mobile_static_menu { + &.container { + display: flex; + flex-direction: column; + align-items: flex-end; + } - .menu { - align-items: flex-end; - position: static; - opacity: 1; - height: 0; - overflow: hidden; - box-shadow: none; - box-sizing: border-box; - padding: 0; - border: none; + .menu { + align-items: flex-end; + position: static; + opacity: 1; + height: 0; + overflow: hidden; + box-shadow: none; + box-sizing: border-box; + padding: 0; + border: none; - button { - margin-right: 15px; + button { + margin-right: 15px; + } } - } - .menu-opened { - height: auto; - border-right: 3px solid black; - border-top: 3px solid black; + .menu-opened { + height: auto; + border-right: 3px solid black; + border-top: 3px solid black; + } } -} \ No newline at end of file +} diff --git a/src/components/Navigation/Dropdown.tsx b/src/components/Navigation/Dropdown.tsx index 12a06bbcc..d4aea15dd 100644 --- a/src/components/Navigation/Dropdown.tsx +++ b/src/components/Navigation/Dropdown.tsx @@ -1,16 +1,21 @@ import style from "./Dropdown.module.scss" import cs from "classnames" -import { MouseEventHandler, PropsWithChildren, useCallback, useState } from "react" +import { + MouseEventHandler, + PropsWithChildren, + useCallback, + useState, +} from "react" import { DropdownMenu } from "./DropdownMenu" import { useClientEffect } from "../../utils/hookts" - interface Props { itemComp: React.ReactNode ariaLabel?: string className?: string btnClassName?: string closeOnClick?: boolean + mobileMenuAbsolute?: boolean } export function Dropdown({ @@ -19,23 +24,26 @@ export function Dropdown({ className, btnClassName, closeOnClick = true, - children + children, + mobileMenuAbsolute, }: PropsWithChildren) { const [opened, setOpened] = useState(false) - const toggle: MouseEventHandler = useCallback((evt) => { - evt.preventDefault() - evt.stopPropagation() - setOpened(!opened) - }, [opened]) + const toggle: MouseEventHandler = useCallback( + (evt) => { + evt.preventDefault() + evt.stopPropagation() + setOpened(!opened) + }, + [opened] + ) useClientEffect(() => { if (opened) { const onClick = (evt: any) => { if (closeOnClick) { setOpened(false) - } - else { + } else { let close = true if (evt.path) { for (const el of evt.path) { @@ -58,19 +66,22 @@ export function Dropdown({ }, [opened]) return ( -
    - - { children } + {children}
    ) -} \ No newline at end of file +} diff --git a/src/components/Navigation/DropdownMenu.tsx b/src/components/Navigation/DropdownMenu.tsx index 2f4683eed..c7d479a43 100644 --- a/src/components/Navigation/DropdownMenu.tsx +++ b/src/components/Navigation/DropdownMenu.tsx @@ -3,20 +3,23 @@ import style from "./Dropdown.module.scss" import effects from "../../styles/Effects.module.scss" import cs from "classnames" - interface Props { opened: boolean className?: string } -export function DropdownMenu({ opened, className, children }: PropsWithChildren) { +export function DropdownMenu({ + opened, + className, + children, +}: PropsWithChildren) { return ( -
    - { children } + {children}
    ) -} \ No newline at end of file +} diff --git a/src/components/Objkt/ObjktImageAndName.tsx b/src/components/Objkt/ObjktImageAndName.tsx index 3a72d3609..d0c78c78e 100644 --- a/src/components/Objkt/ObjktImageAndName.tsx +++ b/src/components/Objkt/ObjktImageAndName.tsx @@ -1,28 +1,31 @@ -import React, { memo, useMemo } from 'react'; -import style from "./ObjktImageAndName.module.scss"; -import { Objkt } from "../../types/entities/Objkt"; -import Link from "next/link"; -import Image from "next/image"; -import { ipfsGatewayUrl } from "../../services/Ipfs"; +import React, { memo, useMemo } from "react" +import style from "./ObjktImageAndName.module.scss" +import { Objkt } from "../../types/entities/Objkt" +import Link from "next/link" +import Image from "next/image" +import { ipfsGatewayUrl } from "../../services/Ipfs" interface Props { - objkt: Objkt, + objkt: Objkt imagePriority?: boolean shortName?: boolean size?: number } -const _ObjtkImageAndName = ({ +const _ObjtkImageAndName = ({ objkt, imagePriority, shortName, size = 40, }: Props) => { - const thumbnailUrl = useMemo(() => ipfsGatewayUrl(objkt.metadata?.thumbnailUri), [objkt]) + const thumbnailUrl = useMemo( + () => ipfsGatewayUrl(objkt.metadata?.thumbnailUri), + [objkt] + ) return ( - {thumbnailUrl && + {thumbnailUrl && ( {`thumbnail - } + )} {shortName ? `#${objkt.iteration}` : objkt.name} - ); -}; + ) +} -export const ObjktImageAndName = memo(_ObjtkImageAndName); +export const ObjktImageAndName = memo(_ObjtkImageAndName) diff --git a/src/components/Offers/OfferActions.tsx b/src/components/Offers/OfferActions.tsx index da81f5f48..94c0c759c 100644 --- a/src/components/Offers/OfferActions.tsx +++ b/src/components/Offers/OfferActions.tsx @@ -28,11 +28,7 @@ interface Props { * It is not render opiniated and the parent component is responsible for * styling. */ -export function OfferActions({ - offer, - objkt, - children, -}: Props) { +export function OfferActions({ offer, objkt, children }: Props) { const { user } = useContext(UserContext) // ensures that objkt is set even if not passed as prop @@ -43,7 +39,7 @@ export function OfferActions({ loading: cancelLoading, error: cancelError, success: cancelSuccess, - call: cancelCall , + call: cancelCall, params: cancelParams, } = useContractOperation(OfferCancelOperation) @@ -59,7 +55,7 @@ export function OfferActions({ loading: acceptLoading, error: acceptError, success: acceptSuccess, - call: acceptCall , + call: acceptCall, params: acceptParams, } = useContractOperation(OfferAcceptOperation) @@ -67,56 +63,66 @@ export function OfferActions({ acceptCall({ offer: offer, token: objktSafe, - price: offer.price + price: offer.price, }) } // the buttons, call to actions for the contracts - const buttons = offer.buyer.id === user?.id ? ( - - ) : objktSafe.owner?.id === user?.id ? ( - - ) : null + const buttons = + offer.buyer.id === user?.id ? ( + + ) : objktSafe.owner?.id === user?.id ? ( + + ) : null // contract feedback component - const feedback = cancelParams?.offer.id === offer.id ? ( - - ) : acceptParams?.offer.id === offer.id ? ( - - ) : null + const feedback = + cancelParams?.offer.id === offer.id ? ( + + ) : acceptParams?.offer.id === offer.id ? ( + + ) : null return children({ buttons, feedback, }) -} \ No newline at end of file +} diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index adba3ada5..8c50f69d0 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -3,11 +3,10 @@ import text from "../../styles/Text.module.css" import cs from "classnames" import { useEffect, useMemo, useState, Fragment } from "react" - interface Props { activePage: number itemsCount: number - itemsPerPage: number, + itemsPerPage: number onChange: (page: number) => void } @@ -15,7 +14,7 @@ export function Pagination({ activePage, itemsCount, itemsPerPage, - onChange + onChange, }: Props) { const nbPages = Math.ceil(itemsCount / itemsPerPage) @@ -25,9 +24,9 @@ export function Pagination({ const P: number[] = [] let D for (let i = 0; i < nbPages; i++) { - D = Math.min(Math.abs(i - activePage), i, (nbPages-1)-i) + D = Math.min(Math.abs(i - activePage), i, nbPages - 1 - i) if (D < 3) { - if (i - P[P.length-1] > 1) P.push(-1) + if (i - P[P.length - 1] > 1) P.push(-1) P.push(i) } } @@ -38,45 +37,45 @@ export function Pagination({
    - {pages.map(page => ( + {pages.map((page) => ( - {page < 0 - ? ... - : ( - - )} + {page < 0 ? ( + ... + ) : ( + + )} ))}
    ) diff --git a/src/components/Reveal/RevealIframe.module.scss b/src/components/Reveal/RevealIframe.module.scss index 59c94d404..bbae2eb08 100644 --- a/src/components/Reveal/RevealIframe.module.scss +++ b/src/components/Reveal/RevealIframe.module.scss @@ -4,8 +4,8 @@ } .iframe_container { - width: min(calc(100vh - 140px - 50px - 48px), 100vw); - height: min(calc(100vh - 140px - 50px - 48px), 100vw); + width: min(calc(100vh - 140px - 50px - 48px), calc(100vw - 20px)); + height: min(calc(100vh - 140px - 50px - 48px), calc(100vw - 20px)); border: 10px solid var(--color-border); background: var(--color-border); position: relative; diff --git a/src/components/Reveal/RevealIframe.tsx b/src/components/Reveal/RevealIframe.tsx index d8c175294..3dce282cc 100644 --- a/src/components/Reveal/RevealIframe.tsx +++ b/src/components/Reveal/RevealIframe.tsx @@ -10,47 +10,45 @@ interface Props { onLoaded?: () => void resetOnUrlChange?: boolean } -export const RevealIframe = forwardRef(({ - url, - onLoaded, - resetOnUrlChange = false -}, ref) => { - const [loaded, setLoaded] = useState(false) +export const RevealIframe = forwardRef( + ({ url, onLoaded, resetOnUrlChange = false }, ref) => { + const [loaded, setLoaded] = useState(false) - useEffect(() => { - if (resetOnUrlChange) { - setLoaded(false) - } - }, [resetOnUrlChange, url]) + useEffect(() => { + if (resetOnUrlChange) { + setLoaded(false) + } + }, [resetOnUrlChange, url]) - const isLoaded = () => { - setTimeout(() => { - setLoaded(true) - onLoaded?.() - }, 500) - } + const isLoaded = () => { + setTimeout(() => { + setLoaded(true) + onLoaded?.() + }, 500) + } - return ( -
    -
    - -