diff --git a/app/api/product/contract-mockup/route.ts b/app/api/product/contract-mockup/route.ts
new file mode 100644
index 0000000..95a33a9
--- /dev/null
+++ b/app/api/product/contract-mockup/route.ts
@@ -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);
+}
diff --git a/components/product/ContractMockup.css b/components/product/ContractMockup.css
new file mode 100644
index 0000000..352afc2
--- /dev/null
+++ b/components/product/ContractMockup.css
@@ -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;
+}
diff --git a/components/product/ContractMockup.tsx b/components/product/ContractMockup.tsx
new file mode 100644
index 0000000..1ed42f3
--- /dev/null
+++ b/components/product/ContractMockup.tsx
@@ -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 (
+
+ {/* MacOS-style window frame */}
+
+
+
+
+
+
+ {snippet?.fileName ?? 'contract.sol'}
+
+
+
+
+
+ {/* Body */}
+
+ {isLoading && (
+
+ {Array.from({ length: 10 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+ {!isLoading && isError && (
+
+
+
+ Failed to load the contract preview.
+
+
+
+ )}
+
+ {!isLoading && !isError && snippet && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/product/__tests__/ContractMockup.test.tsx b/components/product/__tests__/ContractMockup.test.tsx
new file mode 100644
index 0000000..204cfb9
--- /dev/null
+++ b/components/product/__tests__/ContractMockup.test.tsx
@@ -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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ expect(screen.getByText('Copied')).toBeInTheDocument();
+ });
+});
diff --git a/hooks/__tests__/useContractMockup.test.tsx b/hooks/__tests__/useContractMockup.test.tsx
new file mode 100644
index 0000000..77f7a8b
--- /dev/null
+++ b/hooks/__tests__/useContractMockup.test.tsx
@@ -0,0 +1,89 @@
+import { renderHook, waitFor, act } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import type { ReactNode } from 'react';
+import { useContractMockup } from '@/hooks/useContractMockup';
+import { contractMockupService } from '@/services/contractMockupService';
+import type { ContractSnippet } from '@/types/contractMockup';
+
+jest.mock('@/services/contractMockupService');
+
+const mockedService = contractMockupService as jest.Mocked;
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+ return function Wrapper({ children }: { children: ReactNode }) {
+ return (
+ {children}
+ );
+ };
+}
+
+describe('useContractMockup', () => {
+ const mockSnippet: ContractSnippet = {
+ fileName: 'EscrowVault.sol',
+ language: 'solidity',
+ code: 'contract EscrowVault {}',
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ Object.assign(navigator, {
+ clipboard: { writeText: jest.fn().mockResolvedValue(undefined) },
+ });
+ });
+
+ it('loads the contract snippet from the service', async () => {
+ mockedService.getContractSnippet.mockResolvedValueOnce(mockSnippet);
+
+ const { result } = renderHook(() => useContractMockup(), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isLoading).toBe(true);
+
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ expect(result.current.snippet).toEqual(mockSnippet);
+ expect(result.current.isError).toBe(false);
+ });
+
+ it('surfaces an error state when the fetch fails', async () => {
+ mockedService.getContractSnippet.mockRejectedValueOnce(new Error('boom'));
+
+ const { result } = renderHook(() => useContractMockup(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ expect(result.current.isError).toBe(true);
+ expect(result.current.snippet).toBeNull();
+ });
+
+ it('copies the loaded code and flips isCopied for 2 seconds', async () => {
+ jest.useFakeTimers({ legacyFakeTimers: false });
+ mockedService.getContractSnippet.mockResolvedValueOnce(mockSnippet);
+
+ const { result } = renderHook(() => useContractMockup(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ await act(async () => {
+ await result.current.copyCode();
+ });
+
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith(mockSnippet.code);
+ expect(result.current.isCopied).toBe(true);
+
+ act(() => {
+ jest.advanceTimersByTime(2000);
+ });
+
+ expect(result.current.isCopied).toBe(false);
+ jest.useRealTimers();
+ });
+});
diff --git a/hooks/useContractMockup.ts b/hooks/useContractMockup.ts
new file mode 100644
index 0000000..8fd54c1
--- /dev/null
+++ b/hooks/useContractMockup.ts
@@ -0,0 +1,50 @@
+/**
+ * useContractMockup — Hook layer for the product page's IDE contract mockup.
+ *
+ * Architecture: Component → useContractMockup (Hook) → contractMockupService → Backend
+ */
+
+'use client';
+
+import { useCallback, useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { contractMockupService } from '@/services/contractMockupService';
+import type { ContractSnippet } from '@/types/contractMockup';
+
+export interface UseContractMockupResult {
+ snippet: ContractSnippet | null;
+ isLoading: boolean;
+ isError: boolean;
+ isCopied: boolean;
+ refetch: () => void;
+ copyCode: () => Promise;
+}
+
+export function useContractMockup(): UseContractMockupResult {
+ const [isCopied, setIsCopied] = useState(false);
+
+ const query = useQuery({
+ queryKey: ['contract-mockup'],
+ queryFn: ({ signal }) => contractMockupService.getContractSnippet(signal),
+ staleTime: Infinity,
+ retry: false,
+ });
+
+ const snippet = query.data ?? null;
+
+ const copyCode = useCallback(async (): Promise => {
+ if (!snippet) return;
+ await navigator.clipboard.writeText(snippet.code);
+ setIsCopied(true);
+ setTimeout(() => setIsCopied(false), 2000);
+ }, [snippet]);
+
+ return {
+ snippet,
+ isLoading: query.isLoading,
+ isError: query.isError,
+ isCopied,
+ refetch: () => void query.refetch(),
+ copyCode,
+ };
+}
diff --git a/hooks/useUser.ts b/hooks/useUser.ts
index 255afdb..15a8793 100644
--- a/hooks/useUser.ts
+++ b/hooks/useUser.ts
@@ -82,3 +82,5 @@ export function useUser() {
refreshProfile: fetchProfile
};
}
+
+// reviewed
\ No newline at end of file
diff --git a/hooks/useWorkflowCards.ts b/hooks/useWorkflowCards.ts
index c7a4d9b..7b4d00b 100644
--- a/hooks/useWorkflowCards.ts
+++ b/hooks/useWorkflowCards.ts
@@ -8,3 +8,6 @@ export function useWorkflowCards() {
queryFn: () => deliveryWorkflowService.getWorkflowCards(),
});
}
+
+
+// random
diff --git a/package.json b/package.json
index dd9cf09..4943f86 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"lucide-react": "^1.9.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
+ "prismjs": "^1.30.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
@@ -52,6 +53,7 @@
"@types/leaflet": "^1.9.8",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^20.0.0",
+ "@types/prismjs": "^1.26.6",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"autoprefixer": "^10.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 923a947..bd74800 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -59,6 +59,9 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
+ prismjs:
+ specifier: ^1.30.0
+ version: 1.30.0
qrcode.react:
specifier: ^4.2.0
version: 4.2.0(react@19.2.6)
@@ -123,6 +126,9 @@ importers:
'@types/node':
specifier: ^20.0.0
version: 20.19.41
+ '@types/prismjs':
+ specifier: ^1.26.6
+ version: 1.26.6
'@types/react':
specifier: ^19.0.0
version: 19.2.15
@@ -1441,6 +1447,9 @@ packages:
'@types/node@20.19.41':
resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==}
+ '@types/prismjs@1.26.6':
+ resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==}
+
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -3370,6 +3379,10 @@ packages:
resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==}
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
+ prismjs@1.30.0:
+ resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+ engines: {node: '>=6'}
+
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@@ -5458,6 +5471,8 @@ snapshots:
dependencies:
undici-types: 6.21.0
+ '@types/prismjs@1.26.6': {}
+
'@types/react-dom@19.2.3(@types/react@19.2.15)':
dependencies:
'@types/react': 19.2.15
@@ -7711,6 +7726,8 @@ snapshots:
react-is-18: react-is@18.3.1
react-is-19: react-is@19.2.6
+ prismjs@1.30.0: {}
+
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
diff --git a/services/__tests__/contractMockupService.test.ts b/services/__tests__/contractMockupService.test.ts
new file mode 100644
index 0000000..2aac0ce
--- /dev/null
+++ b/services/__tests__/contractMockupService.test.ts
@@ -0,0 +1,50 @@
+jest.mock('axios');
+import axios from 'axios';
+import { contractMockupService } from '@/services/contractMockupService';
+import type { ContractSnippet } from '@/types/contractMockup';
+
+const mockedAxios = axios as jest.Mocked;
+
+describe('contractMockupService', () => {
+ const mockSnippet: ContractSnippet = {
+ fileName: 'EscrowVault.sol',
+ language: 'solidity',
+ code: 'contract EscrowVault {}',
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('fetches the contract snippet from the backend API', async () => {
+ mockedAxios.get.mockResolvedValueOnce({ data: mockSnippet });
+
+ const result = await contractMockupService.getContractSnippet();
+
+ expect(result).toEqual(mockSnippet);
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ expect.stringContaining('/api/product/contract-mockup'),
+ expect.any(Object),
+ );
+ });
+
+ it('forwards an AbortSignal when provided', async () => {
+ const controller = new AbortController();
+ mockedAxios.get.mockResolvedValueOnce({ data: mockSnippet });
+
+ await contractMockupService.getContractSnippet(controller.signal);
+
+ expect(mockedAxios.get).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({ signal: controller.signal }),
+ );
+ });
+
+ it('propagates request failures', async () => {
+ mockedAxios.get.mockRejectedValueOnce(new Error('Network error'));
+
+ await expect(contractMockupService.getContractSnippet()).rejects.toThrow(
+ 'Network error',
+ );
+ });
+});
diff --git a/services/contractMockupService.ts b/services/contractMockupService.ts
new file mode 100644
index 0000000..bd02b8f
--- /dev/null
+++ b/services/contractMockupService.ts
@@ -0,0 +1,18 @@
+import axios from 'axios';
+import type { ContractSnippet } from '@/types/contractMockup';
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? '';
+
+/**
+ * contractMockupService — all contract-mockup-related API communication.
+ * Hooks call this; components never call this directly.
+ */
+export const contractMockupService = {
+ async getContractSnippet(signal?: AbortSignal): Promise {
+ const { data } = await axios.get(
+ `${API_BASE_URL}/api/product/contract-mockup`,
+ { signal },
+ );
+ return data;
+ },
+};
diff --git a/types/contractMockup.ts b/types/contractMockup.ts
new file mode 100644
index 0000000..45b5255
--- /dev/null
+++ b/types/contractMockup.ts
@@ -0,0 +1,11 @@
+/**
+ * Represents a source file rendered inside the IDE-style contract mockup window.
+ */
+export interface ContractSnippet {
+ /** Displayed in the window's title bar, e.g. "EscrowVault.sol" */
+ fileName: string;
+ /** PrismJS language grammar to highlight with, e.g. "solidity" | "typescript" */
+ language: string;
+ /** Raw source code, already de-indented and ready to render verbatim */
+ code: string;
+}