From fe1ff54cd0a58aacce6ce9d921c8cf7199795b0c Mon Sep 17 00:00:00 2001 From: BrickheadJohnny Date: Thu, 28 Nov 2024 23:16:07 +0100 Subject: [PATCH] wip: first working solution --- src/app/create-requirement/page.tsx | 226 ++++++++++++++++++++++++++ src/lib/schemas/integrationBuilder.ts | 85 ++++++++++ 2 files changed, 311 insertions(+) create mode 100644 src/app/create-requirement/page.tsx create mode 100644 src/lib/schemas/integrationBuilder.ts diff --git a/src/app/create-requirement/page.tsx b/src/app/create-requirement/page.tsx new file mode 100644 index 0000000000..4905ab01d4 --- /dev/null +++ b/src/app/create-requirement/page.tsx @@ -0,0 +1,226 @@ +import { ChainIndicator } from "@/components/requirements/ChainIndicator"; +import { DataBlock } from "@/components/requirements/DataBlock"; +import { DataBlockWithCopy } from "@/components/requirements/DataBlockWithCopy"; +import { + Requirement, + RequirementContent, + RequirementFooter, + RequirementImage, +} from "@/components/requirements/Requirement"; +import { RequirementLink } from "@/components/requirements/RequirementLink"; +import { Card } from "@/components/ui/Card"; +import { Label } from "@/components/ui/Label"; +import { CHAINS, type SupportedChainID } from "@/config/chains"; +import type { + Configuration, + Integration, + RequirementDisplayComponentConfig, + RequirementDisplayNodeConfig, +} from "@/lib/schemas/integrationBuilder"; +import { shortenHex } from "@/lib/shortenHex"; +import { GearSix } from "@phosphor-icons/react/dist/ssr"; +import { Fragment } from "react"; + +const isSupportedChain = (chainId?: number): chainId is SupportedChainID => + chainId ? !!CHAINS[chainId as SupportedChainID] : false; + +// These 2 objects will come from our backend +const TEMP_INTEGRATION = { + id: "temp_integration_id", + display_name: "ERC20 Token", + identity_type: "address", + incoming_data_config: { + fields: [ + { + name: "amount", + type: "number", + ops: [ + { + op: "set", + }, + ], + }, + ], + }, +} satisfies Integration; + +const TEMP_CONFIGURATION = { + id: "temp_configuration_id", + integration_id: "temp_integration_id", + data: { + chain: 1, + address: "0xff04820c36759c9f5203021fe051239ad2dcca8a", + amount: 1, + }, +} satisfies Configuration; +const _configurationData = Object.entries(TEMP_CONFIGURATION.data); +const configurationData = isSupportedChain(TEMP_CONFIGURATION.data.chain) + ? _configurationData.filter(([key]) => key !== "chain") + : _configurationData; + +const ADDRESS_REGEX = /^0x[a-f0-9]{40}$/i; + +// This is what the user will build on our UI +const customRequirementDisplayComponent = { + configurationId: "temp_configuration_id", + contentNodes: [ + { + id: "contentNode1", + type: "TEXT", + value: "Hold at least {{amount}} ETH", + }, + ], + footerNodes: [ + { + id: "footerNode1", + type: "CHAIN_INDICATOR", + value: TEMP_CONFIGURATION.data.chain, + }, + { + id: "footerNode2", + type: "EXTERNAL_LINK", + href: "https://etherscan.io/address/{{address}}", + value: "View on explorer", + }, + ], +} satisfies RequirementDisplayComponentConfig; + +const PLACEHOLDER_REGEX = /\{\{([^{}]+)\}\}/g; +const convertTemplateText = ( + templateText: string, + requirementConfiguration: Configuration, +) => + templateText.replace(PLACEHOLDER_REGEX, (_match, rawKey) => { + const key = rawKey.trim(); + return key in requirementConfiguration.data + ? requirementConfiguration.data[key] + : key; + }); + +const renderNode = ( + node: RequirementDisplayNodeConfig, + requirementConfiguration: Configuration, +) => { + switch (node.type) { + case "TEXT": + return ( + + {convertTemplateText(node.value, requirementConfiguration)} + + ); + case "CHAIN_INDICATOR": + return isSupportedChain(node.value) ? ( + + ) : null; + case "EXTERNAL_LINK": + return ( + + {node.value} + + ); + default: + return null; + } +}; + +const CreateRequirementPage = () => { + return ( +
+ +
+

+ Create requirement +

+

+ Set up a custom requirement display component +

+
+ +
+ +
+
+ Data block: + Example +
+ +
+ Data block with copy: + +
+
+
+ +
+ +
+
+ Chain indicator: + +
+ +
+ External link: + View on explorer +
+
+
+ +
+ + + + + + +

+ {`${TEMP_INTEGRATION.display_name} (`} + {configurationData.map(([key, value], index) => ( + + {`${key}: `} + {typeof value === "string" && ADDRESS_REGEX.test(value) ? ( + + {shortenHex(value)} + + ) : ( + {value} + )} + {index < configurationData.length - 1 && {", "}} + + ))} + ) +

+ {isSupportedChain(TEMP_CONFIGURATION.data.chain) && ( + + + + )} +
+
+ + + + +

+ {customRequirementDisplayComponent.contentNodes.map((node) => + renderNode(node, TEMP_CONFIGURATION), + )} +

+ {customRequirementDisplayComponent.footerNodes?.length > 0 && ( + + {customRequirementDisplayComponent.footerNodes.map((node) => + renderNode(node, TEMP_CONFIGURATION), + )} + + )} +
+
+
+
+ ); +}; + +export default CreateRequirementPage; diff --git a/src/lib/schemas/integrationBuilder.ts b/src/lib/schemas/integrationBuilder.ts new file mode 100644 index 0000000000..e5c8a6d9b8 --- /dev/null +++ b/src/lib/schemas/integrationBuilder.ts @@ -0,0 +1,85 @@ +import { z } from "zod"; + +const IdentitySchema = z.enum([ + "address", + "discord", + "telegram", + "github", + "farcaster", +]); // TODO: add all identities + +const PrimitiveTypeSchema = z.string().or(z.number()).or(z.boolean()); +const OpSchema = z.object({ op: z.enum(["add", "set"]) }); +const UUIDSchema = z.string().uuid(); + +const CreateIntegrationSchema = z.object({ + display_name: z.string().min(1), + identity_type: IdentitySchema, + incoming_data_config: z.object({ + fields: z.array( + z.object({ + name: z.string().min(1), + type: z.enum(["string", "number", "boolean"]), + ops: z.array(OpSchema), + }), + ), + }), +}); + +const IntegrationSchema = CreateIntegrationSchema.extend({ + id: UUIDSchema, +}); + +export type Integration = z.infer; + +const CreateConfigurationSchema = z.object({ + integration_id: UUIDSchema, + data: z.record(PrimitiveTypeSchema), +}); + +const ConfigurationSchema = CreateConfigurationSchema.extend({ + id: UUIDSchema, +}); + +export type Configuration = z.infer; + +// ---------- ---------- ---------- ---------- // + +const TextNodeSchema = z.object({ + id: UUIDSchema, + type: z.literal("TEXT"), + value: z.string().min(1), +}); + +const ChainIndicatorNodeSchema = z.object({ + id: UUIDSchema, + type: z.literal("CHAIN_INDICATOR"), + value: z.number().positive(), +}); + +const ExternalLinkNodeSchema = z.object({ + id: UUIDSchema, + type: z.literal("EXTERNAL_LINK"), + href: z.string().url(), + value: z.string().min(1), +}); + +const RequirementDisplayNodeConfigSchema = z.discriminatedUnion("type", [ + TextNodeSchema, + ChainIndicatorNodeSchema, + ExternalLinkNodeSchema, +]); + +export type RequirementDisplayNodeConfig = z.infer< + typeof RequirementDisplayNodeConfigSchema +>; + +const RequirementDisplayComponentConfigSchema = z.object({ + configurationId: UUIDSchema, + contentNodes: z.array(RequirementDisplayNodeConfigSchema), + footerNodes: z.array(RequirementDisplayNodeConfigSchema), +}); + +export type RequirementDisplayComponentConfig = z.infer< + typeof RequirementDisplayComponentConfigSchema +>;