Skip to content
Open
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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [18.x, 20.x]

steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Build
run: npm run build
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ next-env.d.ts

# local clones / tooling
base-bridge/

# IDE
.vscode/
.idea/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Terminally Onchain

![CI](https://github.com/AdekunleBamz/sol2base/actions/workflows/ci.yml/badge.svg)

<div align="center">
<img src="assets/terminally-onchain.png" alt="Terminally Onchain Bridge" width="800" />
</div>
Expand Down
90 changes: 90 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,93 @@ body {
text-transform: uppercase !important;
letter-spacing: 1px !important;
}

/* Additional Animations */
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes pulse-subtle {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}

.animate-slide-in {
animation: slide-in 0.2s ease-out;
}

.animate-fade-in {
animation: fade-in 0.3s ease-out;
}

.animate-pulse-subtle {
animation: pulse-subtle 2s ease-in-out infinite;
}

/* Focus visible styles for accessibility */
:focus-visible {
outline: 2px solid rgba(0, 255, 0, 0.5);
outline-offset: 2px;
}

/* Smooth scrolling */
html {
scroll-behavior: smooth;
}

/* Custom scrollbar for terminal aesthetic */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}

::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}

::-webkit-scrollbar-thumb {
background: rgba(0, 255, 0, 0.3);
border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
background: rgba(0, 255, 0, 0.5);
}

/* Selection styling */
::selection {
background: rgba(0, 255, 0, 0.3);
color: #00ff00;
}

/* Terminal cursor blink */
.terminal-cursor {
display: inline-block;
width: 8px;
height: 1.2em;
background: #00ff00;
animation: blink 1s step-end infinite;
vertical-align: text-bottom;
margin-left: 2px;
}
13 changes: 13 additions & 0 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import { MainContent } from '../components/MainContent';
import { WalletConnection } from '../components/WalletConnection';
import { Footer } from '../components/Footer';
import { NetworkStatus, useNetworkStatus } from '../components/NetworkStatus';
import { KeyboardShortcuts } from '../components/terminal/KeyboardShortcuts';
import { useNetwork } from '../contexts/NetworkContext';
import {
ENVIRONMENT_CHOICES,
Expand All @@ -12,6 +15,7 @@ import {

export default function Home() {
const { config, environment, setEnvironment } = useNetwork();
const { solanaStatus, baseStatus, solanaLatency, baseLatency } = useNetworkStatus();

return (
<div className="min-h-screen bg-[#010104] text-[#aaf7c9] font-mono">
Expand All @@ -27,6 +31,12 @@ export default function Home() {
</p>
</div>
<div className="flex items-center gap-4 flex-wrap justify-end">
<NetworkStatus
solanaStatus={solanaStatus}
baseStatus={baseStatus}
solanaLatency={solanaLatency}
baseLatency={baseLatency}
/>
<label className="text-[11px] uppercase tracking-[0.2em] text-green-300 flex flex-col items-end gap-1">
<span>network</span>
<select
Expand All @@ -41,11 +51,14 @@ export default function Home() {
))}
</select>
</label>
<KeyboardShortcuts />
<WalletConnection />
</div>
</header>

<MainContent />

<Footer />
</div>
</div>
);
Expand Down
186 changes: 186 additions & 0 deletions src/components/CopyableAddress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
'use client';

import React, { useState, useCallback } from 'react';

interface CopyableAddressProps {
/** The full address to display and copy */
address: string;
/** Optional label to show before the address */
label?: string;
/** Number of characters to show at start and end (default: 6) */
truncateLength?: number;
/** Whether to show the full address without truncation */
showFull?: boolean;
/** Custom class name for styling */
className?: string;
/** Type of address for appropriate styling */
type?: 'solana' | 'base' | 'generic';
}

export const CopyableAddress: React.FC<CopyableAddressProps> = ({
address,
label,
truncateLength = 6,
showFull = false,
className = '',
type = 'generic',
}) => {
const [copied, setCopied] = useState(false);
const [showTooltip, setShowTooltip] = useState(false);

const truncatedAddress = showFull
? address
: `${address.slice(0, truncateLength)}...${address.slice(-truncateLength)}`;

const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(address);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy address:', error);
}
}, [address]);

const getTypeStyles = () => {
switch (type) {
case 'solana':
return 'text-purple-400 hover:text-purple-300';
case 'base':
return 'text-blue-400 hover:text-blue-300';
default:
return 'text-green-400 hover:text-green-300';
}
};

const getTypeIcon = () => {
switch (type) {
case 'solana':
return (
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
);
case 'base':
return (
<svg className="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" />
<path d="M12 6v12M6 12h12" stroke="currentColor" strokeWidth="2" />
</svg>
);
default:
return null;
}
};

return (
<div
className={`inline-flex items-center gap-1.5 ${className}`}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
{label && (
<span className="text-green-300/70 text-xs uppercase tracking-wider">
{label}:
</span>
)}

{getTypeIcon() && (
<span className={`${getTypeStyles()} opacity-60`}>{getTypeIcon()}</span>
)}

<button
type="button"
onClick={handleCopy}
className={`relative font-mono text-xs ${getTypeStyles()} transition-colors cursor-pointer bg-transparent border-none p-0 group`}
title={`Click to copy: ${address}`}
aria-label={`Copy address ${truncatedAddress}`}
>
<span className="flex items-center gap-1">
{truncatedAddress}
<svg
className={`w-3 h-3 transition-all ${
copied ? 'text-emerald-400' : 'opacity-0 group-hover:opacity-100'
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{copied ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
)}
</svg>
</span>

{/* Tooltip showing full address */}
{showTooltip && !showFull && (
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 pointer-events-none">
<div className="bg-black/95 border border-green-500/40 rounded px-2 py-1 text-[10px] text-green-200 whitespace-nowrap shadow-lg">
{address}
<div className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-green-500/40" />
</div>
</div>
)}
</button>

{copied && (
<span className="text-emerald-400 text-[10px] animate-fade-in">
Copied!
</span>
)}
</div>
);
};

// Utility component for transaction hashes
interface CopyableTxHashProps {
hash: string;
explorerUrl?: string;
network: 'solana' | 'base';
}

export const CopyableTxHash: React.FC<CopyableTxHashProps> = ({
hash,
explorerUrl,
network,
}) => {
return (
<div className="inline-flex items-center gap-2">
<CopyableAddress
address={hash}
type={network}
truncateLength={8}
/>
{explorerUrl && (
<a
href={explorerUrl}
target="_blank"
rel="noopener noreferrer"
className="text-green-400/60 hover:text-green-300 transition-colors"
aria-label="View on explorer"
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
)}
</div>
);
};
Loading