Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
aelmanaa committed Dec 8, 2024
1 parent 9b7032f commit 1a6b8d5
Show file tree
Hide file tree
Showing 13 changed files with 456 additions and 117 deletions.
77 changes: 30 additions & 47 deletions src/components/CCIP/TutorialBlockchainSelector/ContractAddress.tsx
Original file line number Diff line number Diff line change
@@ -1,75 +1,58 @@
import { useState, useCallback } from "react"
import { utils } from "ethers"
import { useStore } from "@nanostores/react"
import { laneStore, setSourceContract, setDestinationContract } from "@stores/lanes"
import type { DeployedContracts } from "@stores/lanes"
import { utils } from "ethers"
import "./ContractAddress.css"
import { useState, useCallback, useEffect, useMemo } from "react"

interface ContractAddressProps {
type: keyof DeployedContracts
chain: "source" | "destination"
placeholder?: string
placeholder: string
}

export const ContractAddress = ({ type, chain, placeholder }: ContractAddressProps) => {
const state = useStore(laneStore)
const contracts = useMemo(
() => (chain === "source" ? state.sourceContracts : state.destinationContracts),
[chain, state]
)
const setValue = useMemo(() => (chain === "source" ? setSourceContract : setDestinationContract), [chain])

const [inputValue, setInputValue] = useState<string>(contracts[type]?.toString() || "")
const [isDirty, setIsDirty] = useState(false)
const [isUpdating, setIsUpdating] = useState(false)

useEffect(() => {
const newValue = contracts[type]?.toString() || ""
if (newValue !== inputValue) {
setInputValue(newValue)
}
}, [contracts, type, inputValue])
const [inputValue, setInputValue] = useState("")
const [isValid, setIsValid] = useState(true)
const setValue = chain === "source" ? setSourceContract : setDestinationContract

const handleChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const address = e.target.value
if (isUpdating || inputValue === address) return
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.trim()
setInputValue(value)

if (value === "") {
// Reset validation and clear store when empty
setIsValid(true)
setValue(type, "") // Clear the store value
return
}

setInputValue(address)
setIsDirty(true)
// Validate non-empty values
const valid = utils.isAddress(value)
setIsValid(valid)

if (address === "" || utils.isAddress(address)) {
setIsUpdating(true)
try {
await new Promise((resolve) => setTimeout(resolve, 0))
setValue(type, address)
} finally {
setIsUpdating(false)
}
// Only update store if it's a valid address
if (valid) {
setValue(type, value)
} else {
// Clear store if value becomes invalid
setValue(type, "")
}
},
[inputValue, setValue, type, isUpdating]
[type, setValue]
)

const isValidAddress = useCallback((address: string) => {
return address === "" || utils.isAddress(address)
}, [])

const showError = isDirty && inputValue !== "" && !isValidAddress(inputValue)

return (
<div className="contract-address-container">
<input
type="string"
spellCheck="false"
autoComplete="off"
type="text"
value={inputValue}
onChange={handleChange}
onBlur={() => setIsDirty(true)}
placeholder={placeholder || `Enter ${type} address`}
className={`contract-address-input ${showError ? "invalid-address" : ""}`}
placeholder={placeholder}
className={`contract-address-input ${!isValid ? "invalid-address" : ""}`}
/>
{showError && <div className="validation-message">⚠️ Please enter a valid Ethereum address</div>}
{!isValid && <span className="validation-message">Please enter a valid Ethereum address</span>}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.parameters {
background: var(--color-background-secondary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
padding: var(--space-4x);
display: flex;
flex-direction: column;
gap: var(--space-3x);
}

.parametersIntro {
margin-bottom: var(--space-3x);
}

.parameterHelp {
margin-top: var(--space-4x);
padding-top: var(--space-3x);
border-top: 1px solid var(--color-border);
}

.helpItem {
display: flex;
gap: var(--space-2x);
margin-bottom: var(--space-2x);
}

.helpLabel {
font-weight: 600;
color: var(--color-text-primary);
}
106 changes: 106 additions & 0 deletions src/components/CCIP/TutorialBlockchainSelector/DeployTokenStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useStore } from "@nanostores/react"
import { laneStore } from "@stores/lanes"
import { ContractAddress } from "./ContractAddress"
import { TutorialCard } from "../TutorialSetup/TutorialCard"
import { TutorialStep } from "../TutorialSetup/TutorialStep"
import { NetworkCheck } from "../TutorialSetup/NetworkCheck"
import { SolidityParam } from "../TutorialSetup/SolidityParam"
import styles from "./DeployTokenStep.module.css"

interface DeployTokenStepProps {
chain: "source" | "destination"
isEnabled: boolean
}

export const DeployTokenStep = ({ chain }: DeployTokenStepProps) => {
const state = useStore(laneStore)
const network = chain === "source" ? state.sourceNetwork : state.destinationNetwork
const networkInfo = network
? {
name: network.name,
logo: network.logo,
}
: { name: "loading..." }

const content = (
<>
<NetworkCheck network={networkInfo} />

<ol className={styles.steps}>
<TutorialStep title="Configure Remix">
<ul>
<li>Open the "Deploy & Run Transactions" tab</li>
<li>Set Environment to "Injected Provider - MetaMask"</li>
<li>
Select <strong>BurnMintERC677</strong> contract
</li>
</ul>
</TutorialStep>

<TutorialStep title="Set Contract Parameters">
<div className={styles.parametersIntro}>
<p>Configure your token by setting these required parameters in Remix:</p>
</div>

<div className={styles.parameters}>
<SolidityParam
name="name"
type="string"
description="The full name of your token that users will see"
example='"My Cross Chain Token"'
/>
<SolidityParam
name="symbol"
type="string"
description="A short ticker symbol for your token (usually 3-4 letters)"
example='"MCCT"'
/>
<SolidityParam
name="decimals"
type="uint8"
description="Number of decimal places your token will support (18 is standard)"
example="18"
/>
<SolidityParam
name="maxSupply"
type="uint256"
description="The maximum amount of tokens that can ever exist (0 means unlimited)"
example="0"
/>
</div>

<div className={styles.parameterHelp}>
<div className={styles.helpItem}>
<span className={styles.helpLabel}>Parameter Name:</span>
<span>The exact name you'll type in Remix</span>
</div>
<div className={styles.helpItem}>
<span className={styles.helpLabel}>Type:</span>
<span>The Solidity data type required</span>
</div>
<div className={styles.helpItem}>
<span className={styles.helpLabel}>Example:</span>
<span>Click to copy a valid value</span>
</div>
</div>
</TutorialStep>

<TutorialStep title="Deploy Contract">
<ul>
<li>Click "Deploy" and confirm in MetaMask</li>
<li>Copy your token address from "Deployed Contracts"</li>
</ul>
<div className={styles.addressInput}>
<ContractAddress type="token" chain={chain} placeholder="Enter deployed token address" />
</div>
</TutorialStep>
</ol>
</>
)

return (
<TutorialCard title="Deploy Token Contract" description="Configure and deploy your token using Remix IDE">
{content}
</TutorialCard>
)
}
19 changes: 19 additions & 0 deletions src/components/CCIP/TutorialSetup/NetworkCheck.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.networkCheck {
display: flex;
align-items: center;
gap: var(--space-2x);
padding: var(--space-4x);
margin-bottom: var(--space-4x);
background: var(--color-background-secondary);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
}

.networkCheck img {
width: 24px;
height: 24px;
}

.networkCheck strong {
color: var(--color-text-primary);
}
17 changes: 17 additions & 0 deletions src/components/CCIP/TutorialSetup/NetworkCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import styles from "./NetworkCheck.module.css"

interface NetworkCheckProps {
network: {
name: string
logo?: string
}
}

export const NetworkCheck = ({ network }: NetworkCheckProps) => (
<div className={styles.networkCheck}>
{network?.logo && <img src={network.logo} alt={network.name} width={24} height={24} />}
<span>
Ensure MetaMask is connected to <strong>{network?.name || "loading..."}</strong>
</span>
</div>
)
100 changes: 49 additions & 51 deletions src/components/CCIP/TutorialSetup/PrerequisitesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,61 +74,59 @@ export const PrerequisitesCard = () => {

return (
<div className={styles.card}>
<div className={styles.header}>
<h3>Prerequisites</h3>
<div className={styles.progress}>
<span>Required Setup</span>
<div className={styles.progressBar} />
</div>
</div>

<div className={styles.steps}>
{prerequisites.map((step) => (
<div key={step.id} className={`${styles.step} ${activeStep === step.id ? styles.active : ""}`}>
<div className={styles.stepHeader}>
<div className={styles.stepInfo}>
<span className={styles.stepTitle}>{step.title}</span>
<p className={styles.stepDescription}>{step.description}</p>
</div>
<div className={styles.stepActions}>
<StepCheckbox stepId="setup" subStepId={step.checkboxId} />
<button
className={styles.expandButton}
onClick={() => setActiveStep(activeStep === step.id ? null : step.id)}
aria-label={activeStep === step.id ? "Collapse section" : "Expand section"}
>
{activeStep === step.id ? "▼" : "▶"}
</button>
</div>
</div>
<div className={styles.title}>Prerequisites</div>
<div className={styles.requirements}>
<div className={styles.section}>
<div className={styles.sectionTitle}>Wallet Setup</div>
<div className={styles.steps}>
{prerequisites.map((step) => (
<div key={step.id} className={`${styles.step} ${activeStep === step.id ? styles.active : ""}`}>
<div className={styles.stepHeader}>
<div className={styles.stepInfo}>
<span className={styles.stepTitle}>{step.title}</span>
<p className={styles.stepDescription}>{step.description}</p>
</div>
<div className={styles.stepActions}>
<StepCheckbox stepId="setup" subStepId={step.checkboxId} />
<button
className={styles.expandButton}
onClick={() => setActiveStep(activeStep === step.id ? null : step.id)}
aria-label={activeStep === step.id ? "Collapse section" : "Expand section"}
>
{activeStep === step.id ? "▼" : "▶"}
</button>
</div>
</div>

{activeStep === step.id && step.options && (
<div className={styles.optionsGrid}>
{step.options.map((option, idx) => (
<div key={idx} className={styles.optionCard}>
<h4>{option.title}</h4>
<ul className={styles.stepsList}>
{option.steps.map((step, stepIdx) => (
<li key={stepIdx}>{step}</li>
))}
</ul>
{option.link && (
<a
href={option.link.url}
target="_blank"
rel="noopener noreferrer"
className={styles.actionButton}
>
{option.link.text}
<span className={styles.linkArrow}></span>
</a>
)}
{activeStep === step.id && step.options && (
<div className={styles.optionsGrid}>
{step.options.map((option, idx) => (
<div key={idx} className={styles.optionCard}>
<h4>{option.title}</h4>
<ul className={styles.stepsList}>
{option.steps.map((step, stepIdx) => (
<li key={stepIdx}>{step}</li>
))}
</ul>
{option.link && (
<a
href={option.link.url}
target="_blank"
rel="noopener noreferrer"
className={styles.actionButton}
>
{option.link.text}
<span className={styles.linkArrow}></span>
</a>
)}
</div>
))}
</div>
))}
)}
</div>
)}
))}
</div>
))}
</div>
</div>
</div>
)
Expand Down
Loading

0 comments on commit 1a6b8d5

Please sign in to comment.