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
156 changes: 156 additions & 0 deletions frontend/src/components/OnboardingPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/* ─── OnboardingPanel ─────────────────────────────────────────────────────── */

.onboarding-panel {
background: var(--bg-muted);
border: 1px solid var(--border-glass);
border-radius: var(--radius-xl);
padding: var(--space-8) var(--space-8);
backdrop-filter: blur(12px);
animation: emptyStateFadeIn 0.4s ease-out both;
width: 100%;
box-sizing: border-box;
}

.onboarding-panel-header {
text-align: center;
margin-bottom: var(--space-8);
}

.onboarding-panel-title {
font-size: var(--text-xl);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin: 0 0 var(--space-2);
}

.onboarding-panel-subtitle {
font-size: var(--text-base);
color: var(--text-secondary);
margin: 0;
}

/* ─── Steps list ──────────────────────────────────────────────────────────── */

.onboarding-steps {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: var(--space-4);
}

.onboarding-step {
display: grid;
grid-template-columns: 40px 40px 1fr auto;
align-items: center;
gap: var(--space-4);
padding: var(--space-4) var(--space-5);
border-radius: var(--radius-lg);
border: 1px solid var(--border-glass);
background: var(--bg-card);
transition: border-color 0.2s ease, background 0.2s ease;
}

/* Active step */
.onboarding-step--active {
border-color: rgba(0, 240, 255, 0.4);
background: rgba(0, 240, 255, 0.04);
}

/* Completed step */
.onboarding-step--completed {
opacity: 0.6;
}

/* Future (locked) step */
.onboarding-step--future {
opacity: 0.45;
}

/* ─── Step indicator (number / check) ────────────────────────────────────── */

.onboarding-step-indicator {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid var(--border-glass);
font-size: var(--text-sm);
font-weight: var(--font-semibold);
color: var(--text-secondary);
flex-shrink: 0;
}

.onboarding-step--active .onboarding-step-indicator {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}

.onboarding-step--completed .onboarding-step-indicator {
border-color: var(--accent-cyan);
background: var(--accent-cyan-dim);
color: var(--accent-cyan);
}

.onboarding-step-check,
.onboarding-step-number {
line-height: 1;
}

/* ─── Step icon ───────────────────────────────────────────────────────────── */

.onboarding-step-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
flex-shrink: 0;
}

.onboarding-step--active .onboarding-step-icon {
color: var(--accent-cyan);
}

/* ─── Step content ────────────────────────────────────────────────────────── */

.onboarding-step-title {
font-size: var(--text-base);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin: 0 0 var(--space-1);
}

.onboarding-step-description {
font-size: var(--text-sm);
color: var(--text-secondary);
margin: 0;
line-height: var(--leading-relaxed);
}

/* ─── Step action button ──────────────────────────────────────────────────── */

.onboarding-step-action {
white-space: nowrap;
flex-shrink: 0;
}

/* ─── Responsive ──────────────────────────────────────────────────────────── */

@media (max-width: 640px) {
.onboarding-panel {
padding: var(--space-6) var(--space-4);
}

.onboarding-step {
grid-template-columns: 32px 32px 1fr;
grid-template-rows: auto auto;
}

.onboarding-step-action {
grid-column: 1 / -1;
width: 100%;
}
}
91 changes: 91 additions & 0 deletions frontend/src/components/OnboardingPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import OnboardingPanel from "./OnboardingPanel";

function renderPanel(walletConnected = false, overrides = {}) {
const props = {
walletConnected,
onConnectWallet: vi.fn(),
onReviewVault: vi.fn(),
onDeposit: vi.fn(),
...overrides,
};
render(<OnboardingPanel {...props} />);
return props;
}

describe("OnboardingPanel", () => {
it("renders all three step titles", () => {
renderPanel();
expect(screen.getByText("Connect Your Wallet")).toBeInTheDocument();
expect(screen.getByText("Review Vault Details")).toBeInTheDocument();
expect(screen.getByText("Make Your First Deposit")).toBeInTheDocument();
});

it("has accessible region label", () => {
renderPanel();
expect(screen.getByRole("region", { name: "Getting started guide" })).toBeInTheDocument();
});

it("marks step 1 as active (aria-current=step) when wallet is not connected", () => {
renderPanel(false);
const items = screen.getAllByRole("listitem");
expect(items[0]).toHaveAttribute("aria-current", "step");
expect(items[1]).not.toHaveAttribute("aria-current");
expect(items[2]).not.toHaveAttribute("aria-current");
});

it("marks step 2 as active when wallet is connected", () => {
renderPanel(true);
const items = screen.getAllByRole("listitem");
expect(items[0]).not.toHaveAttribute("aria-current");
expect(items[1]).toHaveAttribute("aria-current", "step");
});

it("shows 'Connect Wallet' button label when wallet is not connected", () => {
renderPanel(false);
expect(screen.getByRole("button", { name: "Connect Wallet" })).toBeInTheDocument();
});

it("shows 'Connected βœ“' and disables step 1 button when wallet is connected", () => {
renderPanel(true);
const btn = screen.getByRole("button", { name: "Connected βœ“" });
expect(btn).toBeDisabled();
});

it("calls onConnectWallet when Connect Wallet button is clicked", () => {
const { onConnectWallet } = renderPanel(false);
fireEvent.click(screen.getByRole("button", { name: "Connect Wallet" }));
expect(onConnectWallet).toHaveBeenCalledTimes(1);
});

it("calls onReviewVault when View Vault button is clicked (wallet connected)", () => {
const { onReviewVault } = renderPanel(true);
fireEvent.click(screen.getByRole("button", { name: "View Vault" }));
expect(onReviewVault).toHaveBeenCalledTimes(1);
});

it("disables future steps when wallet is not connected", () => {
renderPanel(false);
expect(screen.getByRole("button", { name: "View Vault" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Deposit Now" })).toBeDisabled();
});

it("step 1 has completed class when wallet is connected", () => {
renderPanel(true);
const items = screen.getAllByRole("listitem");
expect(items[0].className).toContain("onboarding-step--completed");
});

it("step 2 has active class when wallet is connected", () => {
renderPanel(true);
const items = screen.getAllByRole("listitem");
expect(items[1].className).toContain("onboarding-step--active");
});

it("step 3 has future class when wallet is connected", () => {
renderPanel(true);
const items = screen.getAllByRole("listitem");
expect(items[2].className).toContain("onboarding-step--future");
});
});
122 changes: 122 additions & 0 deletions frontend/src/components/OnboardingPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import React from "react";
import { Wallet, Layers, TrendingUp } from "./icons";
import "./OnboardingPanel.css";

interface OnboardingStep {
step: number;
icon: React.ReactNode;
title: string;
description: string;
actionLabel: string;
onAction: () => void;
completed: boolean;
}

interface OnboardingPanelProps {
walletConnected: boolean;
onConnectWallet: () => void;
onReviewVault: () => void;
onDeposit: () => void;
}

const OnboardingPanel: React.FC<OnboardingPanelProps> = ({
walletConnected,
onConnectWallet,
onReviewVault,
onDeposit,
}) => {
const steps: OnboardingStep[] = [
{
step: 1,
icon: <Wallet size={24} />,
title: "Connect Your Wallet",
description: "Link your Freighter wallet to get started with YieldVault.",
actionLabel: walletConnected ? "Connected βœ“" : "Connect Wallet",
onAction: onConnectWallet,
completed: walletConnected,
},
{
step: 2,
icon: <Layers size={24} />,
title: "Review Vault Details",
description: "Explore APY, strategy, and TVL before committing funds.",
actionLabel: "View Vault",
onAction: onReviewVault,
completed: false,
},
{
step: 3,
icon: <TrendingUp size={24} />,
title: "Make Your First Deposit",
description: "Deposit USDC to start earning yield backed by real-world assets.",
actionLabel: "Deposit Now",
onAction: onDeposit,
completed: false,
},
];

const activeStep = walletConnected ? 1 : 0;

return (
<div className="onboarding-panel" role="region" aria-label="Getting started guide">
<div className="onboarding-panel-header">
<h2 className="onboarding-panel-title">Get Started with YieldVault</h2>
<p className="onboarding-panel-subtitle">
Follow these steps to start earning institutional-grade yield on your USDC.
</p>
</div>

<ol className="onboarding-steps" aria-label="Onboarding steps">
{steps.map((s, idx) => {
const isActive = idx === activeStep;
const isPast = s.completed;
const isFuture = idx > activeStep && !s.completed;

return (
<li
key={s.step}
className={[
"onboarding-step",
isPast ? "onboarding-step--completed" : "",
isActive ? "onboarding-step--active" : "",
isFuture ? "onboarding-step--future" : "",
]
.filter(Boolean)
.join(" ")}
aria-current={isActive ? "step" : undefined}
>
<div className="onboarding-step-indicator" aria-hidden="true">
{isPast ? (
<span className="onboarding-step-check">βœ“</span>
) : (
<span className="onboarding-step-number">{s.step}</span>
)}
</div>

<div className="onboarding-step-icon" aria-hidden="true">
{s.icon}
</div>

<div className="onboarding-step-content">
<h3 className="onboarding-step-title">{s.title}</h3>
<p className="onboarding-step-description">{s.description}</p>
</div>

<button
type="button"
className={`btn ${isActive ? "btn-primary" : "btn-outline"} onboarding-step-action`}
onClick={s.onAction}
disabled={isPast || isFuture}
aria-label={s.actionLabel}
>
{s.actionLabel}
</button>
</li>
);
})}
</ol>
</div>
);
};

export default OnboardingPanel;
Loading
Loading