Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions app/api/product/contract-mockup/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// DEV-ONLY DEMO ROUTE — delete before opening the PR.
// This stands in for the real backend so the product page's IDE mockup
// renders with data on localhost.
import { NextResponse } from 'next/server';
import type { ContractSnippet } from '@/types/contractMockup';

const fixture: ContractSnippet = {
fileName: 'EscrowVault.sol',
language: 'solidity',
code: `// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title SwiftChain Escrow Vault
/// @notice Locks shipment funds until delivery is confirmed on-chain.
contract EscrowVault {
enum Status { Pending, Locked, Released, Disputed }

struct Escrow {
address payer;
address payee;
uint256 amount;
Status status;
}

mapping(bytes32 => Escrow) public escrows;

event FundsLocked(bytes32 indexed escrowId, uint256 amount);
event FundsReleased(bytes32 indexed escrowId, address indexed payee);

function lockFunds(bytes32 escrowId, address payee) external payable {
require(msg.value > 0, "Amount must be greater than zero");
require(escrows[escrowId].status == Status.Pending, "Escrow already exists");

escrows[escrowId] = Escrow({
payer: msg.sender,
payee: payee,
amount: msg.value,
status: Status.Locked
});

emit FundsLocked(escrowId, msg.value);
}

function releaseFunds(bytes32 escrowId) external {
Escrow storage escrow = escrows[escrowId];
require(escrow.status == Status.Locked, "Escrow not locked");
require(msg.sender == escrow.payer, "Only payer can release");

escrow.status = Status.Released;
payable(escrow.payee).transfer(escrow.amount);

emit FundsReleased(escrowId, escrow.payee);
}
}
`,
};

export async function GET() {
return NextResponse.json(fixture);
}
52 changes: 52 additions & 0 deletions components/product/ContractMockup.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* Dark-mode-optimized Solidity/TypeScript token colors for the IDE mockup. */
.contract-mockup-code .token.comment,
.contract-mockup-code .token.block-comment,
.contract-mockup-code .token.prolog,
.contract-mockup-code .token.doctype,
.contract-mockup-code .token.cdata {
color: #6a737d;
font-style: italic;
}

.contract-mockup-code .token.keyword,
.contract-mockup-code .token.builtin {
color: #c586c0;
}

.contract-mockup-code .token.string,
.contract-mockup-code .token.char {
color: #ce9178;
}

.contract-mockup-code .token.number,
.contract-mockup-code .token.boolean {
color: #b5cea8;
}

.contract-mockup-code .token.function {
color: #dcdcaa;
}

.contract-mockup-code .token.class-name {
color: #4ec9b0;
}

.contract-mockup-code .token.operator,
.contract-mockup-code .token.punctuation {
color: #d4d4d4;
}

.contract-mockup-code .token.property,
.contract-mockup-code .token.parameter {
color: #9cdcfe;
}

.contract-mockup-code .token.constant,
.contract-mockup-code .token.symbol {
color: #4fc1ff;
}

.contract-mockup-code .token.annotation,
.contract-mockup-code .token.decorator {
color: #d7ba7d;
}
116 changes: 116 additions & 0 deletions components/product/ContractMockup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use client';

/**
* ContractMockup — MacOS-style IDE window showcasing a Solidity escrow
* contract on the product/marketing page, with copy-to-clipboard.
*
* Architecture: ContractMockup (Component) → useContractMockup (Hook) →
* contractMockupService (Service) → Backend
*/

import { useMemo } from 'react';
import Prism from 'prismjs';
import 'prismjs/components/prism-solidity';
import 'prismjs/components/prism-typescript';
import { Copy, Check, AlertCircle } from 'lucide-react';
import { useContractMockup } from '@/hooks/useContractMockup';
import './ContractMockup.css';

export function ContractMockup() {
const { snippet, isLoading, isError, isCopied, refetch, copyCode } =
useContractMockup();

const highlightedHtml = useMemo(() => {
if (!snippet) return '';
const grammar = Prism.languages[snippet.language] ?? Prism.languages.solidity;
return Prism.highlight(snippet.code, grammar, snippet.language);
}, [snippet]);

return (
<div
className="w-full max-w-2xl overflow-hidden rounded-xl border border-gray-800 bg-[#1e1e1e] shadow-2xl"
role="group"
aria-label="Smart contract code preview"
>
{/* MacOS-style window frame */}
<div className="flex items-center gap-2 border-b border-gray-800 bg-[#2d2d2d] px-4 py-3">
<span className="h-3 w-3 rounded-full bg-[#FF5F56]" aria-hidden="true" />
<span className="h-3 w-3 rounded-full bg-[#FFBD2E]" aria-hidden="true" />
<span className="h-3 w-3 rounded-full bg-[#27C93F]" aria-hidden="true" />

<span className="flex-1 truncate text-center font-mono text-xs text-gray-400">
{snippet?.fileName ?? 'contract.sol'}
</span>

<button
type="button"
onClick={() => void copyCode()}
disabled={!snippet}
aria-label="Copy contract code"
className={[
'inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors',
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-[#2d2d2d]',
snippet
? 'text-gray-400 hover:bg-white/10 hover:text-gray-200'
: 'cursor-not-allowed text-gray-600',
].join(' ')}
>
{isCopied ? (
<>
<Check className="h-3.5 w-3.5" aria-hidden="true" />
Copied
</>
) : (
<>
<Copy className="h-3.5 w-3.5" aria-hidden="true" />
Copy
</>
)}
</button>
</div>

{/* Body */}
<div className="max-h-[420px] overflow-auto px-4 py-4">
{isLoading && (
<div className="space-y-2.5" aria-label="Loading contract code">
{Array.from({ length: 10 }).map((_, i) => (
<div
key={i}
className="h-3 animate-pulse rounded bg-gray-700/50"
style={{ width: `${60 + ((i * 13) % 35)}%` }}
/>
))}
</div>
)}

{!isLoading && isError && (
<div
role="alert"
className="flex flex-col items-center gap-3 py-8 text-center"
>
<AlertCircle className="h-6 w-6 text-red-400" aria-hidden="true" />
<p className="text-sm text-gray-400">
Failed to load the contract preview.
</p>
<button
type="button"
onClick={refetch}
className="rounded-md bg-white/10 px-3 py-1.5 text-xs font-medium text-gray-200 hover:bg-white/20"
>
Retry
</button>
</div>
)}

{!isLoading && !isError && snippet && (
<pre className="contract-mockup-code overflow-x-auto font-mono text-[13px] leading-relaxed">
<code
className={`language-${snippet.language}`}
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
/>
</pre>
)}
</div>
</div>
);
}
109 changes: 109 additions & 0 deletions components/product/__tests__/ContractMockup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ContractMockup } from '@/components/product/ContractMockup';
import { useContractMockup } from '@/hooks/useContractMockup';

jest.mock('@/hooks/useContractMockup');
jest.mock('../ContractMockup.css', () => ({}));

const mockedUseContractMockup = useContractMockup as jest.Mock;

describe('ContractMockup', () => {
const mockSnippet = {
fileName: 'EscrowVault.sol',
language: 'solidity',
code: 'contract EscrowVault { uint256 public amount; }',
};

beforeEach(() => {
jest.clearAllMocks();
});

it('renders the MacOS-style window frame with the file name', () => {
mockedUseContractMockup.mockReturnValue({
snippet: mockSnippet,
isLoading: false,
isError: false,
isCopied: false,
refetch: jest.fn(),
copyCode: jest.fn(),
});

render(<ContractMockup />);

expect(screen.getByText('EscrowVault.sol')).toBeInTheDocument();
expect(
screen.getByRole('group', { name: /smart contract code preview/i }),
).toBeInTheDocument();
});

it('shows a loading skeleton while the snippet is being fetched', () => {
mockedUseContractMockup.mockReturnValue({
snippet: null,
isLoading: true,
isError: false,
isCopied: false,
refetch: jest.fn(),
copyCode: jest.fn(),
});

render(<ContractMockup />);

expect(screen.getByLabelText(/loading contract code/i)).toBeInTheDocument();
});

it('shows an error state with a retry action on failure', async () => {
const refetch = jest.fn();
mockedUseContractMockup.mockReturnValue({
snippet: null,
isLoading: false,
isError: true,
isCopied: false,
refetch,
copyCode: jest.fn(),
});

render(<ContractMockup />);

expect(
screen.getByText(/failed to load the contract preview/i),
).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', { name: /retry/i }));
expect(refetch).toHaveBeenCalledTimes(1);
});

it('invokes copyCode when the copy button is clicked', async () => {
const copyCode = jest.fn();
mockedUseContractMockup.mockReturnValue({
snippet: mockSnippet,
isLoading: false,
isError: false,
isCopied: false,
refetch: jest.fn(),
copyCode,
});

render(<ContractMockup />);

await userEvent.click(
screen.getByRole('button', { name: /copy contract code/i }),
);
expect(copyCode).toHaveBeenCalledTimes(1);
});

it('reflects the copied state from the hook', () => {
mockedUseContractMockup.mockReturnValue({
snippet: mockSnippet,
isLoading: false,
isError: false,
isCopied: true,
refetch: jest.fn(),
copyCode: jest.fn(),
});

render(<ContractMockup />);

expect(screen.getByText('Copied')).toBeInTheDocument();
});
});
Loading
Loading