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
108 changes: 72 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,85 +6,121 @@ on:
pull_request:
branches: [main, dev]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
contracts: ${{ steps.filter.outputs.contracts }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'
contracts:
- 'contracts/**'

backend:
needs: changes
if: ${{ needs.changes.outputs.backend == 'true' }}
name: Backend (Node.js ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
defaults:
run:
working-directory: backend
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node-version }}
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: backend/package-lock.json

- name: Install dependencies
run: npm install
working-directory: backend

- name: Run ESLint
run: npm run lint
working-directory: backend
run: npm ci

- name: Run tests
run: npm run test
working-directory: backend
- name: Lint & Test
run: |
npm run lint
npm run test

- name: Build
run: npm run build
working-directory: backend

frontend:
needs: changes
if: ${{ needs.changes.outputs.frontend == 'true' }}
name: Frontend (Node.js ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
defaults:
run:
working-directory: frontend
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node-version }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm install
working-directory: frontend
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/frontend/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('frontend/package-lock.json') }}-${{ hashFiles('frontend/src/**') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('frontend/package-lock.json') }}-

- name: Run ESLint
run: npm run lint
working-directory: frontend
- name: Install dependencies
run: npm ci

- name: Run tests
run: npm run test
working-directory: frontend
- name: Lint & Test
run: |
npm run lint
npm run test

- name: Build
run: npm run build
working-directory: frontend

contracts:
needs: changes
if: ${{ needs.changes.outputs.contracts == 'true' }}
name: Smart Contracts (Rust)
runs-on: ubuntu-latest
container: rust:1.88
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/checkout@v4

- name: Build contracts
run: cargo build --release
working-directory: contracts
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown

- name: Check contracts
run: cargo check
working-directory: contracts
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "contracts -> target"

- name: Build & Check
working-directory: contracts
run: |
cargo build --target wasm32-unknown-unknown --release
cargo check
2 changes: 0 additions & 2 deletions backend/src/routes/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {
InvalidStellarInputError,
} from '../services/stellar.js';



export const stellarRouter = Router();

// Get Stellar account info
Expand Down
155 changes: 35 additions & 120 deletions frontend/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,11 @@ import {
import { getDashboardBreadcrumbs } from '@/lib/breadcrumbs';
import { ThemeSettingsModal } from '@/components/theme/ThemeSettingsModal';

// 2. I built the isolated NetworkIndicator component right here
const NetworkIndicator = () => {
// We use useAccount() in Wagmi v2 instead of useNetwork()
const { chain, isConnected } = useAccount();

// Hide it if the wallet isn't connected yet
if (!isConnected) return null;

// In v2, if connected but 'chain' is undefined, it means they are on an unsupported network
if (!chain) {
return (
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-red-100 text-red-700 text-sm font-medium border border-red-200">
Expand All @@ -50,43 +46,9 @@ const NetworkIndicator = () => {
);
}

// Handle Mainnet (Green) vs Testnet (Yellow) states
const isTestnet = chain.testnet === true;
const bgColor = isTestnet
? 'bg-yellow-100 text-yellow-800 border-yellow-200'
: 'bg-green-100 text-green-800 border-green-200';
const dotColor = isTestnet ? 'bg-yellow-500' : 'bg-green-500';

return (
<div className={`flex items-center gap-2 px-3 py-1 rounded-full border text-sm font-medium ${bgColor}`}>
<span className={`w-2 h-2 rounded-full ${dotColor}`}></span>
{chain.name}
</div>
);
};

// 2. I built the isolated NetworkIndicator component right here
const NetworkIndicator = () => {
// We use useAccount() in Wagmi v2 instead of useNetwork()
const { chain, isConnected } = useAccount();

// Hide it if the wallet isn't connected yet
if (!isConnected) return null;

// In v2, if connected but 'chain' is undefined, it means they are on an unsupported network
if (!chain) {
return (
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-red-100 text-red-700 text-sm font-medium border border-red-200">
<span className="w-2 h-2 rounded-full bg-red-500 animate-pulse"></span>
Wrong Network
</div>
);
}

// Handle Mainnet (Green) vs Testnet (Yellow) states
const isTestnet = chain.testnet === true;
const bgColor = isTestnet
? 'bg-yellow-100 text-yellow-800 border-yellow-200'
const bgColor = isTestnet
? 'bg-yellow-100 text-yellow-800 border-yellow-200'
: 'bg-green-100 text-green-800 border-green-200';
const dotColor = isTestnet ? 'bg-yellow-500' : 'bg-green-500';

Expand Down Expand Up @@ -125,7 +87,6 @@ export function Header() {
const handleManualToggle = () => {
const next = !isDark;
setIsDark(next);
// Apply to DOM immediately (ThemeProvider's effect will also fire)
document.documentElement.classList.toggle('dark', next);
};

Expand All @@ -151,43 +112,8 @@ export function Header() {
</h1>
</div>

<div className="flex items-center gap-4">

{/* 3. I dropped the new component right here! */}
<NetworkIndicator />

<div className="flex items-center gap-2">
{/* Notifications */}
<Button variant="ghost" size="icon" className="relative">
<Bell className="h-5 w-5" />
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
</Button>

{/* Dark mode toggle — only interactive label when manual */}
<Button
variant="ghost"
size="icon"
onClick={mode === 'manual' ? handleManualToggle : undefined}
title={
mode === 'manual'
? isDark
? 'Switch to light mode'
: 'Switch to dark mode'
: `Auto: ${mode} mode`
}
className="relative"
>
{isDark ? (
<Moon className="h-5 w-5 transition-transform duration-300" />
) : (
<Sun className="h-5 w-5 transition-transform duration-300" />
)}
{mode !== 'manual' && (
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full bg-primary flex items-center justify-center">
<Clock className="h-2 w-2 text-primary-foreground" />
</span>
)}
</Button>
<div className="flex items-center gap-4">
<NetworkIndicator />

{/* Theme schedule settings */}
<Button
Expand All @@ -199,47 +125,37 @@ export function Header() {
<Clock className="h-5 w-5" />
</Button>

{/* User menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex items-center gap-3 h-auto py-2 px-3">
<Avatar className="h-8 w-8">
<AvatarFallback className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
{initials}
</AvatarFallback>
</Avatar>
<div className="hidden sm:block text-left">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{name || 'User'}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{shortAddress}</p>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium">{name || 'User'}</p>
<p className="text-xs text-gray-500">{email || 'No email'}</p>
<p className="text-xs text-gray-400 font-mono">{shortAddress}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<User className="mr-2 h-4 w-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem>
<Settings className="mr-2 h-4 w-4" />
Settings
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} className="text-red-600">
<LogOut className="mr-2 h-4 w-4" />
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex items-center gap-3 h-auto py-2 px-3">
<Avatar className="h-8 w-8">
<AvatarFallback className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
{initials}
</AvatarFallback>
</Avatar>
<div className="hidden sm:block text-left">
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{name || 'User'}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{shortAddress}</p>
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium">{name || 'User'}</p>
<p className="text-xs text-gray-500">{email || 'No email'}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem><User className="mr-2 h-4 w-4" /> Profile</DropdownMenuItem>
<DropdownMenuItem><Settings className="mr-2 h-4 w-4" /> Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} className="text-red-600">
<LogOut className="mr-2 h-4 w-4" /> Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
</div>
Expand All @@ -264,7 +180,6 @@ export function Header() {
</div>
)}
</header>

<ThemeSettingsModal open={themeSettingsOpen} onClose={() => setThemeSettingsOpen(false)} />
</>
);
Expand Down
Loading