Skip to content

Commit

Permalink
wip: first working solution
Browse files Browse the repository at this point in the history
  • Loading branch information
BrickheadJohnny committed Nov 28, 2024
1 parent 5494dee commit fe1ff54
Show file tree
Hide file tree
Showing 2 changed files with 311 additions and 0 deletions.
226 changes: 226 additions & 0 deletions src/app/create-requirement/page.tsx
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;
85 changes: 85 additions & 0 deletions src/lib/schemas/integrationBuilder.ts
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
>;

0 comments on commit fe1ff54

Please sign in to comment.