-
Notifications
You must be signed in to change notification settings - Fork 440
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5494dee
commit fe1ff54
Showing
2 changed files
with
311 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<span key={node.id}> | ||
{convertTemplateText(node.value, requirementConfiguration)} | ||
</span> | ||
); | ||
case "CHAIN_INDICATOR": | ||
return isSupportedChain(node.value) ? ( | ||
<ChainIndicator key={node.id} chain={node.value} /> | ||
) : null; | ||
case "EXTERNAL_LINK": | ||
return ( | ||
<RequirementLink | ||
key={node.id} | ||
href={convertTemplateText(node.href, requirementConfiguration)} | ||
> | ||
{node.value} | ||
</RequirementLink> | ||
); | ||
default: | ||
return null; | ||
} | ||
}; | ||
|
||
const CreateRequirementPage = () => { | ||
return ( | ||
<main className="container mx-auto max-w-md py-16"> | ||
<Card className="grid gap-6 px-5 py-6 shadow-lg md:px-6"> | ||
<div className="grid gap-2"> | ||
<h1 className="text-center font-display font-extrabold text-2xl"> | ||
Create requirement | ||
</h1> | ||
<p className="text-center text-foreground-secondary"> | ||
Set up a custom requirement display component | ||
</p> | ||
</div> | ||
|
||
<div className="grid gap-1.5"> | ||
<Label>Available content blocks:</Label> | ||
<div className="grid gap-1"> | ||
<div className="flex items-center gap-1"> | ||
<span>Data block:</span> | ||
<DataBlock>Example</DataBlock> | ||
</div> | ||
|
||
<div className="flex items-center gap-1"> | ||
<span>Data block with copy:</span> | ||
<DataBlockWithCopy text="Example" /> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div className="grid gap-1.5"> | ||
<Label>Available footer blocks:</Label> | ||
<div className="grid gap-1"> | ||
<div className="flex items-center gap-1"> | ||
<span>Chain indicator:</span> | ||
<ChainIndicator chain={1} /> | ||
</div> | ||
|
||
<div className="flex items-center gap-1"> | ||
<span>External link:</span> | ||
<RequirementLink href="#">View on explorer</RequirementLink> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<hr /> | ||
|
||
<Requirement className="rounded-2xl border border-border bg-card-secondary p-5"> | ||
<RequirementImage> | ||
<GearSix className="size-6" /> | ||
</RequirementImage> | ||
<RequirementContent> | ||
<p> | ||
<span>{`${TEMP_INTEGRATION.display_name} (`}</span> | ||
{configurationData.map(([key, value], index) => ( | ||
<Fragment key={key}> | ||
<span>{`${key}: `}</span> | ||
{typeof value === "string" && ADDRESS_REGEX.test(value) ? ( | ||
<DataBlockWithCopy text={value}> | ||
{shortenHex(value)} | ||
</DataBlockWithCopy> | ||
) : ( | ||
<span>{value}</span> | ||
)} | ||
{index < configurationData.length - 1 && <span>{", "}</span>} | ||
</Fragment> | ||
))} | ||
<span>)</span> | ||
</p> | ||
{isSupportedChain(TEMP_CONFIGURATION.data.chain) && ( | ||
<RequirementFooter> | ||
<ChainIndicator chain={TEMP_CONFIGURATION.data.chain} /> | ||
</RequirementFooter> | ||
)} | ||
</RequirementContent> | ||
</Requirement> | ||
|
||
<Requirement className="rounded-2xl border border-border bg-card-secondary p-5"> | ||
<RequirementImage /> | ||
<RequirementContent> | ||
<p> | ||
{customRequirementDisplayComponent.contentNodes.map((node) => | ||
renderNode(node, TEMP_CONFIGURATION), | ||
)} | ||
</p> | ||
{customRequirementDisplayComponent.footerNodes?.length > 0 && ( | ||
<RequirementFooter> | ||
{customRequirementDisplayComponent.footerNodes.map((node) => | ||
renderNode(node, TEMP_CONFIGURATION), | ||
)} | ||
</RequirementFooter> | ||
)} | ||
</RequirementContent> | ||
</Requirement> | ||
</Card> | ||
</main> | ||
); | ||
}; | ||
|
||
export default CreateRequirementPage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof IntegrationSchema>; | ||
|
||
const CreateConfigurationSchema = z.object({ | ||
integration_id: UUIDSchema, | ||
data: z.record(PrimitiveTypeSchema), | ||
}); | ||
|
||
const ConfigurationSchema = CreateConfigurationSchema.extend({ | ||
id: UUIDSchema, | ||
}); | ||
|
||
export type Configuration = z.infer<typeof ConfigurationSchema>; | ||
|
||
// ---------- ---------- ---------- ---------- // | ||
|
||
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 | ||
>; |