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
1,664 changes: 1,612 additions & 52 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@
"axios": "^1.18.0",
"clsx": "^2.1.1",
"framer-motion": "^12.40.0",
"highlight.js": "^11.11.1",
"lucide-react": "^1.18.0",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-markdown": "^10.1.0",
"react-router-dom": "^7.17.0",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
"zustand": "^5.0.14"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/highlight.js": "^9.12.4",
"@types/node": "^24.13.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
Expand Down
109 changes: 109 additions & 0 deletions src/components/docs/DocContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeHighlight from 'rehype-highlight'
import type { Components } from 'react-markdown'

const COPY_STYLES: Components = {
code({ className, children, ...props }) {
const isInline = !className
if (isInline) {
return (
<code className="px-1.5 py-0.5 rounded-md bg-elevated text-brand font-mono text-sm" {...props}>
{children}
</code>
)
}
return (
<code className={className} {...props}>
{children}
</code>
)
},
pre({ children }) {
return (
<pre className="overflow-x-auto rounded-xl border border-border bg-surface/80 p-4 text-sm leading-relaxed">
{children}
</pre>
)
},
table({ children }) {
return (
<div className="overflow-x-auto my-6">
<table className="w-full text-sm border-collapse">{children}</table>
</div>
)
},
th({ children }) {
return (
<th className="text-left py-3 px-4 text-text-muted font-medium border-b border-border bg-elevated/50">
{children}
</th>
)
},
td({ children }) {
return (
<td className="py-3 px-4 text-text-secondary border-b border-border/50">{children}</td>
)
},
a({ href, children }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-brand underline underline-offset-2 decoration-brand/60 hover:decoration-brand"
>
{children}
</a>
)
},
h1({ children }) {
return <h1 className="font-display font-bold text-3xl text-text-primary mb-6 mt-0">{children}</h1>
},
h2({ children }) {
return <h2 className="font-display font-bold text-xl text-text-primary mb-4 mt-10 pb-2 border-b border-border">{children}</h2>
},
h3({ children }) {
return <h3 className="font-display font-semibold text-lg text-text-primary mb-3 mt-8">{children}</h3>
},
p({ children }) {
return <p className="text-text-secondary leading-relaxed mb-4">{children}</p>
},
ul({ children }) {
return <ul className="space-y-2 mb-4 ml-5 list-disc text-text-secondary">{children}</ul>
},
ol({ children }) {
return <ol className="space-y-2 mb-4 ml-5 list-decimal text-text-secondary">{children}</ol>
},
li({ children }) {
return <li className="leading-relaxed">{children}</li>
},
blockquote({ children }) {
return (
<blockquote className="border-l-4 border-brand/40 pl-4 py-2 my-4 rounded-r-xl bg-brand/5 text-text-secondary">
{children}
</blockquote>
)
},
hr() {
return <hr className="border-border my-8" />
},
}

interface DocContentProps {
content: string
}

export function DocContent({ content }: DocContentProps) {
return (
<div className="markdown-body">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight]}
components={COPY_STYLES}
>
{content}
</ReactMarkdown>
</div>
)
}
17 changes: 17 additions & 0 deletions src/components/docs/DocSections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BookOpen, Rocket, DollarSign, Store, Award, Binary, Code } from 'lucide-react'

export interface DocSection {
slug: string
title: string
icon: typeof BookOpen
}

export const DOC_SECTIONS: DocSection[] = [
{ slug: 'introduction', title: 'Introduction', icon: BookOpen },
{ slug: 'getting-started', title: 'Getting Started', icon: Rocket },
{ slug: 'for-sponsors', title: 'For Sponsors', icon: DollarSign },
{ slug: 'for-vendors', title: 'For Vendors', icon: Store },
{ slug: 'for-mentors', title: 'For Mentors', icon: Award },
{ slug: 'smart-contracts', title: 'Smart Contracts', icon: Binary },
{ slug: 'api-reference', title: 'API Reference', icon: Code },
]
59 changes: 59 additions & 0 deletions src/components/docs/DocSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { clsx } from 'clsx'
import { DOC_SECTIONS } from './DocSections'

interface DocSidebarProps {
activeSlug: string
onNavigate: (slug: string) => void
mobileOpen: boolean
onMobileClose: () => void
}

export function DocSidebar({ activeSlug, onNavigate, mobileOpen, onMobileClose }: DocSidebarProps) {
return (
<>
{mobileOpen && (
<div
className="fixed inset-0 bg-black/50 z-30 lg:hidden"
onClick={onMobileClose}
aria-hidden="true"
/>
)}

<aside
className={clsx(
'fixed lg:sticky top-20 lg:top-20 z-40 lg:z-0 h-[calc(100vh-5rem)]',
'w-64 shrink-0 border-r border-border bg-bg overflow-y-auto',
'transition-transform duration-300 lg:translate-x-0',
mobileOpen ? 'translate-x-0' : '-translate-x-full'
)}
aria-label="Documentation sidebar"
>
<nav className="p-4 space-y-1">
{DOC_SECTIONS.map((section) => {
const Icon = section.icon
const isActive = activeSlug === section.slug
return (
<button
key={section.slug}
onClick={() => {
onNavigate(section.slug)
onMobileClose()
}}
className={clsx(
'w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all text-left',
isActive
? 'bg-brand/10 text-brand border border-brand/20'
: 'text-text-secondary hover:text-text-primary hover:bg-surface border border-transparent'
)}
aria-current={isActive ? 'page' : undefined}
>
<Icon size={16} />
{section.title}
</button>
)
})}
</nav>
</aside>
</>
)
}
10 changes: 4 additions & 6 deletions src/components/layout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,10 @@ export function Footer() {
className="hover:text-brand transition-colors">
GitHub
</a>
<a href="https://stepfi.vercel.app/docs"
target="_blank"
rel="noopener noreferrer"
className="hover:text-brand transition-colors">
Docs
</a>
<Link to="/docs"
className="hover:text-brand transition-colors">
Docs
</Link>
<Link to="/contracts"
className="hover:text-brand transition-colors">
Contracts
Expand Down
122 changes: 122 additions & 0 deletions src/docs/api-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# API Reference

The StepFi REST API provides programmatic access to all protocol features. The base URL is:

```
https://stepfi-api.onrender.com/api/v1
```

## Authentication

Most endpoints require a JWT token obtained through wallet authentication.

```typescript
// Get nonce
const { nonce } = await authService.getNonce(walletAddress)

// Sign with Freighter and verify
const { token } = await authService.verify({
walletAddress,
signedMessage: signedNonce,
})
```

Include the token in subsequent requests:

```bash
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
https://stepfi-api.onrender.com/api/v1/pool/info
```

## Endpoints

### Pool

```typescript
GET /api/v1/pool/info
```

Returns the current liquidity pool state:

```json
{
"totalDeposits": 48320,
"totalShares": 45200,
"sharePrice": 1.069,
"availableLiquidity": 31200,
"lockedLiquidity": 17120,
"apy": 0.124
}
```

### Reputation

```typescript
GET /api/v1/reputation/score/:walletAddress
GET /api/v1/reputation/profile/:walletAddress
GET /api/v1/reputation/history/:walletAddress
GET /api/v1/reputation/learner/:walletAddress/loans
GET /api/v1/reputation/:walletAddress/vouches
POST /api/v1/reputation/vouch/request
```

### Loans

```typescript
GET /api/v1/loans/my
GET /api/v1/loans/:id
```

### Vendors

```typescript
GET /api/v1/vendors
GET /api/v1/vendors/:id
GET /api/v1/vendors/dashboard
GET /api/v1/vendors/loans
GET /api/v1/vendors/payments
GET /api/v1/vendors/products
POST /api/v1/vendors/api-keys
DELETE /api/v1/vendors/api-keys/:id
```

### Vouching

```typescript
GET /api/v1/vouches/requests
GET /api/v1/vouches/my
POST /api/v1/vouches/submit
POST /api/v1/vouches/revoke/:id
```

## Error Handling

The API returns standard HTTP status codes:

```text
200 — Success
201 — Created
400 — Bad request (validation error)
401 — Unauthorized (invalid or expired token)
404 — Not found
500 — Internal server error
```

Error responses include a message:

```json
{
"error": "Invalid wallet address",
"message": "The provided wallet address is not a valid Stellar public key."
}
```

## Rate Limiting

API requests are rate-limited to 100 requests per minute per IP address. The response headers include:

```text
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1718000000
```
Loading
Loading