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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ stellar contract invoke \
--resolver <RESOLVER_ADDRESS>
```

Replace `<CONTRACT_ID>` with the ID from the previous step and `<RESOLVER_ADDRESS>` with the admin's Stellar public key. This call can only succeed once any subsequent call will panic.
Replace `<CONTRACT_ID>` with the ID from the previous step and `<RESOLVER_ADDRESS>` with the admin's Stellar public key. This call can only succeed once - any subsequent call will panic.

---

Expand Down Expand Up @@ -228,7 +228,7 @@ The freelancer calls `mark_complete` after finishing work on a `Funded` mileston
| `mark_complete(caller, project_id, milestone_index)` | Freelancer | Signals work is done; transitions `Funded` → `Completed` |
| `approve_milestone(project_id, milestone_index, token)` | Client | Releases escrowed XLM to the freelancer (must be `Completed`) |
| `dispute_milestone(caller, project_id, milestone_index)` | Client or Freelancer | Flags milestone as `Disputed`, funds stay locked |
| `resolve_dispute(caller, project_id, milestone_index, token, release_to_freelancer)` | Resolver | Settles a `Disputed` milestone pays winner, marks `Released` |
| `resolve_dispute(caller, project_id, milestone_index, token, release_to_freelancer)` | Resolver | Settles a `Disputed` milestone - pays winner, marks `Released` |
| `get_project_count()` | None | Returns total number of projects |
| `get_project(project_id)` | None | Fetch a full project by ID |
| `get_milestone(project_id, milestone_index)` | None | Fetch a single milestone |
Expand All @@ -245,14 +245,16 @@ npm install

# Copy environment variables
cp .env.example .env.local
# Fill in values see Environment Variables section below
# Fill in values - see Environment Variables section below

# Start the development server
npm run dev
```

Open [http://localhost:3000](http://localhost:3000).

On first use, connect your Freighter wallet and select your role (**I want to hire** or **I want to work**). The role is saved in `localStorage` and determines your navigation, dashboard, and landing page CTAs. You can switch roles at any time via the role chip in the navbar.

See [`mile-stack-frontend/README.md`](./mile-stack-frontend/README.md) for full frontend documentation including Supabase setup.

---
Expand All @@ -275,7 +277,7 @@ Create `.env.local` inside `mile-stack-frontend/` using `.env.example` as a temp

## Demo Accounts

Pre-created and funded testnet accounts for testing and live demos. Testnet only no real value.
Pre-created and funded testnet accounts for testing and live demos. Testnet only - no real value.

| Role | Public Key | Secret Key |
|------|-----------|------------|
Expand Down
4 changes: 2 additions & 2 deletions contracts/mile-stack/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ impl MileStackContract {

/// Flag a funded milestone as disputed, locking XLM until resolved.
/// Either the project client or the milestone freelancer may call this.
/// Milestone must be Funded. No funds are moved escrow stays locked.
/// Milestone must be Funded. No funds are moved - escrow stays locked.
pub fn dispute_milestone(
env: Env,
caller: Address,
Expand All @@ -302,7 +302,7 @@ impl MileStackContract {
"milestone must be Funded to dispute"
);

// Funds remain in the contract no token transfer here.
// Funds remain in the contract - no token transfer here.
let updated = Milestone {
status: MilestoneStatus::Disputed,
..milestone
Expand Down
4 changes: 2 additions & 2 deletions contracts/mile-stack/src/test/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use soroban_sdk::{testutils::Address as _, token::Client as TokenClient, Address

#[test]
fn test_only_client_can_fund() {
// No mock_all_auths client auth is NOT provided.
// No mock_all_auths - client auth is NOT provided.
let env = make_env();
let contract_id = env.register(MileStackContract, ());
let contract = MileStackContractClient::new(&env, &contract_id);
Expand All @@ -21,7 +21,7 @@ fn test_only_client_can_fund() {

#[test]
fn test_only_client_can_approve() {
// No mock_all_auths client auth is NOT provided.
// No mock_all_auths - client auth is NOT provided.
let env = make_env();
let contract_id = env.register(MileStackContract, ());
let contract = MileStackContractClient::new(&env, &contract_id);
Expand Down
2 changes: 1 addition & 1 deletion contracts/mile-stack/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod resolve_dispute;
mod types;
mod view_functions;

// Shared test helpers available to all submodules via `use super::helpers::*`
// Shared test helpers - available to all submodules via `use super::helpers::*`
pub mod helpers {
use crate::{MileStackContract, MileStackContractClient, MilestoneStatus, Milestone, Project, DataKey};
use soroban_sdk::{
Expand Down
2 changes: 1 addition & 1 deletion contracts/mile-stack/src/test/resolve_dispute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn test_initialize_sets_resolver() {
let resolver = Address::generate(&env);
contract.initialize(&resolver);

// Verify resolver is stored attempt to re-initialize should panic
// Verify resolver is stored - attempt to re-initialize should panic
let result = contract.try_initialize(&resolver);
assert!(result.is_err(), "double initialize must fail");
}
Expand Down
2 changes: 1 addition & 1 deletion mile-stack-frontend/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

# This is NOT the Next.js you know

This version has breaking changes APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
This version has breaking changes - APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.

<!-- END:nextjs-agent-rules -->
56 changes: 31 additions & 25 deletions mile-stack-frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MileStack Frontend

Next.js 16 frontend for [MileStack](../README.md) a Soroban-powered milestone-based XLM escrow platform connecting developers in the Global South with global employers on Stellar.
Next.js 16 frontend for [MileStack](../README.md) - a Soroban-powered milestone-based XLM escrow platform connecting developers in the Global South with global employers on Stellar.

---

Expand Down Expand Up @@ -37,7 +37,7 @@ Next.js 16 frontend for [MileStack](../README.md) — a Soroban-powered mileston
mile-stack-frontend/
├── app/
│ ├── client/
│ │ ├── page.tsx # Client Dashboard listings + escrow projects
│ │ ├── page.tsx # Client Dashboard - listings + escrow projects
│ │ ├── listings/
│ │ │ ├── new/
│ │ │ │ └── page.tsx # Post a new project listing
Expand All @@ -48,36 +48,40 @@ mile-stack-frontend/
│ │ ├── new/
│ │ │ └── page.tsx # Direct project creation (known freelancer)
│ │ └── [id]/
│ │ ├── page.tsx # Server Component resolves dynamic params
│ │ └── ProjectManage.tsx # Client Component fund / approve / dispute
│ │ ├── page.tsx # Server Component - resolves dynamic params
│ │ └── ProjectManage.tsx # Client Component - fund / approve / dispute
│ ├── freelancer/
│ │ ├── page.tsx # Freelancer Dashboard active projects
│ │ ├── page.tsx # Freelancer Dashboard - active projects
│ │ └── projects/[id]/
│ │ ├── page.tsx # Server Component resolves dynamic params
│ │ └── ProjectDetail.tsx # Client Component milestones + mark complete + dispute
│ │ ├── page.tsx # Server Component - resolves dynamic params
│ │ └── ProjectDetail.tsx # Client Component - milestones + mark complete + dispute
│ ├── projects/
│ │ ├── page.tsx # Public listings browser (no wallet required)
│ │ └── [id]/
│ │ ├── page.tsx # Server Component resolves dynamic params
│ │ └── ListingDetail.tsx # Client Component listing detail + apply
│ │ ├── page.tsx # Server Component - resolves dynamic params
│ │ └── ListingDetail.tsx # Client Component - listing detail + apply
│ ├── globals.css # Design system tokens, animations, base styles
│ ├── icon.svg # App favicon
│ ├── layout.tsx # Root layout font, providers, metadata
│ ├── layout.tsx # Root layout - font, providers, metadata
│ └── page.tsx # Landing page (hero, features, how it works, CTA)
├── components/
│ ├── ui/
│ │ ├── Button.tsx # primary / accent / outline / ghost / destructive variants
│ │ └── Badge.tsx # Milestone status badges (Pending/Funded/Completed/Released/Disputed)
│ ├── Footer.tsx # Site footer
│ ├── Navbar.tsx # Sticky nav with active link highlight + mobile menu
│ ├── Navbar.tsx # Sticky nav with role-adaptive links, role chip, and mobile menu
│ ├── Notification.tsx # Toast notification provider + useNotification hook
│ ├── ScrollReveal.tsx # IntersectionObserver scroll animation wrapper
│ └── WalletGuard.tsx # Wallet connection gate — wraps pages that require Freighter
│ ├── WalletGuard.tsx # Wallet connection gate - wraps pages that require Freighter
│ ├── RoleSelector.tsx # Role selection modal shown after wallet connect (client / freelancer)
│ ├── LandingHeroCta.tsx # Role-adaptive hero CTA buttons
│ └── LandingBottomCta.tsx # Role-adaptive bottom CTA section
├── contexts/
│ └── WalletContext.tsx # Freighter wallet state — connect / disconnect / auto-restore
│ ├── WalletContext.tsx # Freighter wallet state - connect / disconnect / auto-restore
│ └── RoleContext.tsx # Role state ("client" | "freelancer") with localStorage persistence
├── lib/
│ ├── contract.ts # Soroban contract queries and transactions
│ ├── listings.ts # Supabase CRUD listings, applications, project metadata
│ ├── listings.ts # Supabase CRUD - listings, applications, project metadata
│ └── supabase.ts # Lazy Supabase client singleton
├── public/
│ └── favicon.svg
Expand Down Expand Up @@ -195,26 +199,28 @@ Open [http://localhost:3000](http://localhost:3000).

### Client

1. Connect Freighter wallet
1. Connect Freighter wallet → select **I want to hire** in the role selector
2. Go to **Client** dashboard → **New Project**
3. Fill in title, description, skills, and milestones
4. Freelancers apply from the public **/projects** page
5. Review applications at **Client > listings > applications**
6. Accept one application Freighter signs the on-chain `create_project` transaction
6. Accept one application - Freighter signs the on-chain `create_project` transaction
7. Fund each milestone from the project management page
8. Wait for the freelancer to mark the milestone complete (`mark_complete`)
9. Approve the milestone XLM is released to the freelancer
9. Approve the milestone - XLM is released to the freelancer

### Freelancer

1. Connect Freighter wallet
1. Connect Freighter wallet → select **I want to work** in the role selector
2. Browse open projects at **/projects** (no wallet required to view)
3. Apply to a listing with an optional cover message
4. Once accepted, the project appears on the **Freelancer** dashboard
5. After completing work on a `Funded` milestone, click **Mark as Complete**
6. The client can now approve the milestone and release payment
7. Dispute a milestone if needed

> **Switching roles:** The navbar shows a role chip (e.g. `Freelancer ⇄`) that re-opens the role selector so you can switch without disconnecting your wallet.

---

## Design System
Expand All @@ -225,7 +231,7 @@ The design system lives in `app/globals.css` as CSS custom properties, mapped to

| Token | Hex | Usage |
| -------------------- | --------- | --------------------------- |
| `--primary` | `#1E3A5F` | Brand navy headings, logo |
| `--primary` | `#1E3A5F` | Brand navy - headings, logo |
| `--accent` | `#0369A1` | CTA buttons, links |
| `--success` | `#059669` | Released milestone state |
| `--destructive` | `#DC2626` | Disputed state, errors |
Expand All @@ -240,16 +246,16 @@ The design system lives in `app/globals.css` as CSS custom properties, mapped to

| Status | Style |
| --------- | ------------------------------------------------------- |
| Pending | Gray `bg-slate-100 text-slate-600` |
| Funded | Blue `bg-blue-50 text-accent border-blue-200` |
| Completed | Amber `bg-amber-50 text-amber-700 border-amber-200` |
| Released | Green `bg-emerald-50 text-success border-emerald-200` |
| Disputed | Red `bg-red-50 text-destructive border-red-200` |
| Pending | Gray - `bg-slate-100 text-slate-600` |
| Funded | Blue - `bg-blue-50 text-accent border-blue-200` |
| Completed | Amber - `bg-amber-50 text-amber-700 border-amber-200` |
| Released | Green - `bg-emerald-50 text-success border-emerald-200` |
| Disputed | Red - `bg-red-50 text-destructive border-red-200` |

### Rules

- No gradients, no purple anywhere
- All icons from `lucide-react` no emojis
- All icons from `lucide-react` - no emojis
- Light theme only
- Transitions: 150–200ms ease, flat color shifts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export default function ApplicationsPage() {
address,
listing.title,
);
notify(`Project #${projectId} created freelancer accepted.`, "success");
notify(`Project #${projectId} created - freelancer accepted.`, "success");
router.push(`/client/projects/${projectId}`);
} catch (err) {
console.error("[Applications] handleAccept:", err);
Expand Down
2 changes: 1 addition & 1 deletion mile-stack-frontend/app/client/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function ProjectCard({ project, name }: { project: ContractProject; name?: strin
<p className="text-sm text-muted-foreground">
Freelancer:{" "}
<span className="font-medium text-foreground tabular-nums">
{project.milestones[0] ? truncateAddress(project.milestones[0].freelancer) : ""}
{project.milestones[0] ? truncateAddress(project.milestones[0].freelancer) : "-"}
</span>
</p>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ function MilestoneCard({
{showWaitingForFreelancer && (
<div className="border-t border-border pt-4">
<p className="text-xs text-muted-foreground">
Funds are in escrow — waiting for the freelancer to mark this milestone complete.
Funds are in escrow--. Waiting for the freelancer to mark this milestone complete.
</p>
</div>
)}
Expand Down Expand Up @@ -272,7 +272,7 @@ export function ProjectManage({ projectId }: { projectId: number }) {
setFundingIndex(milestoneIndex);
try {
await fundMilestone(address, projectId, milestoneIndex);
notify("Milestone funded XLM is now held in escrow.", "success");
notify("Milestone funded--. XLM is now held in escrow.", "success");
await fetchProject();
} catch (err) {
console.error("[ProjectManage] handleFund:", err);
Expand All @@ -287,7 +287,7 @@ export function ProjectManage({ projectId }: { projectId: number }) {
setApprovingIndex(milestoneIndex);
try {
await approveMilestone(address, projectId, milestoneIndex);
notify("Milestone approved XLM released to the freelancer.", "success");
notify("Milestone approved--. XLM released to the freelancer.", "success");
await fetchProject();
} catch (err) {
console.error("[ProjectManage] handleApprove:", err);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export function ProjectDetail({ projectId }: { projectId: number }) {
setMarkingCompleteIndex(milestoneIndex);
try {
await markComplete(address, projectId, milestoneIndex);
notify("Milestone marked as complete — waiting for client approval.", "success");
notify("Milestone marked as complete--. Waiting for client approval.", "success");
await fetchProject();
} catch (err) {
console.error("[ProjectDetail] handleMarkComplete:", err);
Expand All @@ -203,7 +203,7 @@ export function ProjectDetail({ projectId }: { projectId: number }) {
setDisputingIndex(milestoneIndex);
try {
await disputeMilestone(address, projectId, milestoneIndex);
notify("Dispute raised — funds are now locked until resolved.", "success");
notify("Dispute raised--. Funds are now locked until resolved.", "success");
await fetchProject();
} catch (err) {
console.error("[ProjectDetail] handleDispute:", err);
Expand Down
2 changes: 1 addition & 1 deletion mile-stack-frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@import "tailwindcss";

/* MileStack Design System Professional Navy Light Theme */
/* MileStack Design System - Professional Navy Light Theme */
:root {
--background: #f8fafc;
--foreground: #0f172a;
Expand Down
11 changes: 9 additions & 2 deletions mile-stack-frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { Plus_Jakarta_Sans } from "next/font/google";
import { NotificationProvider } from "@/components/Notification";
import { WalletProvider } from "@/contexts/WalletContext";
import { RoleProvider } from "@/contexts/RoleContext";
import { RoleSelector } from "@/components/RoleSelector";
import "./globals.css";

const plusJakartaSans = Plus_Jakarta_Sans({
Expand All @@ -12,7 +14,7 @@ const plusJakartaSans = Plus_Jakarta_Sans({
});

export const metadata: Metadata = {
title: "MileStack - Milestone-Based Escrow for Global Talent",
title: "MileStack",
description:
"Connect with global employers through milestone-based XLM escrow payments on Stellar. Built for developers and digital professionals in the Global South.",
};
Expand All @@ -26,7 +28,12 @@ export default function RootLayout({
<html lang="en" className={`${plusJakartaSans.variable} h-full`}>
<body className="min-h-full flex flex-col antialiased">
<NotificationProvider>
<WalletProvider>{children}</WalletProvider>
<WalletProvider>
<RoleProvider>
<RoleSelector />
{children}
</RoleProvider>
</WalletProvider>
</NotificationProvider>
</body>
</html>
Expand Down
Loading
Loading