diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cbce830 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + push: + branches: [main, develop, 'perf/**', 'feat/**', 'fix/**'] + pull_request: + branches: [main, develop] + +jobs: + ci: + name: Type Check · Lint · Test · Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + # ── Static Analysis ─────────────────────────────────────────────────── + - name: Type check + run: npm run type-check + + - name: Lint + run: npm run lint + + - name: Format check + run: npm run format:check + + # ── Unit Tests ──────────────────────────────────────────────────────── + - name: Unit tests + run: npm run test + + # ── Build ───────────────────────────────────────────────────────────── + # Validates that Tailwind purges correctly and no runtime CSS fallbacks + # are needed. A failed build here catches missing class issues early. + - name: Build + run: npm run build + env: + # Provide stub values so next build doesn't fail on missing env vars + NEXTAUTH_SECRET: ci-stub-secret + NEXTAUTH_URL: http://localhost:3000 + NEXT_PUBLIC_API_BASE_URL: http://localhost:4000 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de816f4..07ba25f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,27 +29,32 @@ Thank you for your interest in contributing to StellarAid! This guide will help 1. **Fork the repository** on GitHub. 2. **Clone your fork:** + ```bash git clone https://github.com//stellarAid-web.git cd stellarAid-web ``` 3. **Add upstream remote:** + ```bash git remote add upstream https://github.com/Dfunder/stellarAid-web.git ``` 4. **Install dependencies:** + ```bash npm install ``` 5. **Copy the environment file:** + ```bash cp .env.example .env.local ``` 6. **Start the development server:** + ```bash npm run dev ``` @@ -59,16 +64,16 @@ Thank you for your interest in contributing to StellarAid! This guide will help ### Useful Scripts -| Command | Description | -|---------|-------------| -| `npm run dev` | Start development server | -| `npm run build` | Production build | -| `npm run lint` | Run ESLint | -| `npm run type-check` | Run TypeScript type checking | -| `npm run test` | Run unit tests with Vitest | -| `npm run test:watch` | Run tests in watch mode | -| `npm run format` | Format code with Prettier | -| `npm run cypress:open` | Open Cypress E2E tests | +| Command | Description | +| ---------------------- | ---------------------------- | +| `npm run dev` | Start development server | +| `npm run build` | Production build | +| `npm run lint` | Run ESLint | +| `npm run type-check` | Run TypeScript type checking | +| `npm run test` | Run unit tests with Vitest | +| `npm run test:watch` | Run tests in watch mode | +| `npm run format` | Format code with Prettier | +| `npm run cypress:open` | Open Cypress E2E tests | --- @@ -138,13 +143,13 @@ stellarAid-web/ Use descriptive branch names with the following prefixes: -| Prefix | Usage | Example | -|--------|-------|---------| -| `feat/` | New features | `feat/dark-mode-toggle` | -| `fix/` | Bug fixes | `fix/wallet-balance-refresh` | -| `chore/` | Maintenance tasks | `chore/update-dependencies` | -| `docs/` | Documentation | `docs/api-reference` | -| `refactor/` | Code refactoring | `refactor/commission-hooks` | +| Prefix | Usage | Example | +| ----------- | ----------------- | ---------------------------- | +| `feat/` | New features | `feat/dark-mode-toggle` | +| `fix/` | Bug fixes | `fix/wallet-balance-refresh` | +| `chore/` | Maintenance tasks | `chore/update-dependencies` | +| `docs/` | Documentation | `docs/api-reference` | +| `refactor/` | Code refactoring | `refactor/commission-hooks` | --- @@ -213,11 +218,7 @@ export default function MyComponent({ title, onSelect }: MyComponentProps) { // Effects // Handlers // Render - return ( -
- {/* Content */} -
- ); + return
{/* Content */}
; } ``` @@ -235,12 +236,14 @@ export default function MyComponent({ title, onSelect }: MyComponentProps) { ### Before Submitting 1. **Sync with upstream:** + ```bash git fetch upstream git rebase upstream/main ``` 2. **Run checks:** + ```bash npm run lint npm run type-check @@ -268,13 +271,16 @@ export default function MyComponent({ title, onSelect }: MyComponentProps) { ```markdown ## Summary + Brief description of changes. ## Changes + - Change 1 - Change 2 ## Testing + How to test these changes. Closes #issue_number diff --git a/README.md b/README.md index 565c858..19f1cbd 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,14 @@ The platform combines the power of blockchain transparency, decentralized paymen ## ✨ Core Features -* 🎨 Portfolio Showcase — Build a professional creative portfolio -* 💼 Client Marketplace — Connect with businesses and potential clients -* 💸 Instant Payments — Receive payments through Stellar assets -* 🔐 Decentralized & Secure — Wallet-based authentication and ownership -* 📄 Commission Management — Custom work requests and project tracking -* ⭐ Reputation System — Reviews, ratings, and verified work history -* 📊 Transparent Transactions — Payment records verifiable on-chain -* ⚙️ Open Source Ecosystem — Built for developers and community contributors +- 🎨 Portfolio Showcase — Build a professional creative portfolio +- 💼 Client Marketplace — Connect with businesses and potential clients +- 💸 Instant Payments — Receive payments through Stellar assets +- 🔐 Decentralized & Secure — Wallet-based authentication and ownership +- 📄 Commission Management — Custom work requests and project tracking +- ⭐ Reputation System — Reviews, ratings, and verified work history +- 📊 Transparent Transactions — Payment records verifiable on-chain +- ⚙️ Open Source Ecosystem — Built for developers and community contributors --- @@ -33,23 +33,23 @@ Frontend Marketplace & Portfolio Platform ### Features -* Artist portfolio management -* Creative marketplace -* Design discovery & search -* Client dashboards -* Commission request system -* Wallet integration -* Messaging system -* Artist hiring workflow +- Artist portfolio management +- Creative marketplace +- Design discovery & search +- Client dashboards +- Commission request system +- Wallet integration +- Messaging system +- Artist hiring workflow ### Tech Stack -* Next.js -* TypeScript -* TailwindCSS -* React Query -* Zustand -* Stellar Wallet SDK +- Next.js +- TypeScript +- TailwindCSS +- React Query +- Zustand +- Stellar Wallet SDK --- @@ -59,24 +59,24 @@ Backend APIs & Core Services ### Features -* Authentication & authorization -* User profile management -* Portfolio management -* Commission workflows -* Marketplace services -* Messaging services -* Payment tracking -* Notification system -* Analytics & reporting +- Authentication & authorization +- User profile management +- Portfolio management +- Commission workflows +- Marketplace services +- Messaging services +- Payment tracking +- Notification system +- Analytics & reporting ### Tech Stack -* NestJS -* PostgreSQL -* Redis -* Prisma ORM -* BullMQ -* Nodemailer +- NestJS +- PostgreSQL +- Redis +- Prisma ORM +- BullMQ +- Nodemailer --- @@ -86,20 +86,20 @@ Soroban Smart Contracts ### Features -* Escrow payments -* Milestone-based releases -* Artist payouts -* Platform fee management -* Commission agreements -* Dispute handling -* Multi-asset support +- Escrow payments +- Milestone-based releases +- Artist payouts +- Platform fee management +- Commission agreements +- Dispute handling +- Multi-asset support ### Tech Stack -* Rust -* Soroban SDK -* Soroban CLI -* Stellar RPC +- Rust +- Soroban SDK +- Soroban CLI +- Stellar RPC --- @@ -109,18 +109,18 @@ Mobile Application ### Features -* Artist portfolio management -* Client communication -* Real-time notifications -* Project tracking -* Wallet integration -* Secure payments +- Artist portfolio management +- Client communication +- Real-time notifications +- Project tracking +- Wallet integration +- Secure payments ### Tech Stack -* React Native -* Expo -* Stellar SDK +- React Native +- Expo +- Stellar SDK --- @@ -130,11 +130,11 @@ Shared Design Components ### Features -* UI components -* Design tokens -* Brand assets -* Typography standards -* Accessibility guidelines +- UI components +- Design tokens +- Brand assets +- Typography standards +- Accessibility guidelines --- @@ -191,19 +191,19 @@ Lumora leverages Stellar's fast, low-cost, and borderless payment infrastructure Supported assets include: -* XLM -* USDC -* NGNT -* EURC -* Custom Stellar Assets +- XLM +- USDC +- NGNT +- EURC +- Custom Stellar Assets ### Benefits -* Near-instant settlements -* Low transaction fees -* Global accessibility -* Transparent payment history -* Secure wallet ownership +- Near-instant settlements +- Low transaction fees +- Global accessibility +- Transparent payment history +- Secure wallet ownership --- @@ -211,36 +211,36 @@ Supported assets include: ## 🌐 Frontend -* Next.js -* React -* TypeScript -* TailwindCSS -* React Query -* Zustand -* Stellar Wallet SDK +- Next.js +- React +- TypeScript +- TailwindCSS +- React Query +- Zustand +- Stellar Wallet SDK ## ⚙️ Backend -* NestJS -* PostgreSQL -* Redis -* Prisma ORM -* BullMQ -* Nodemailer +- NestJS +- PostgreSQL +- Redis +- Prisma ORM +- BullMQ +- Nodemailer ## 📝 Smart Contracts -* Rust -* Soroban SDK -* Soroban CLI -* Stellar RPC +- Rust +- Soroban SDK +- Soroban CLI +- Stellar RPC ## 🗄 Storage Layer -* PostgreSQL -* Redis -* Object Storage (S3 Compatible) -* Optional IPFS Support +- PostgreSQL +- Redis +- Object Storage (S3 Compatible) +- Optional IPFS Support --- @@ -250,57 +250,57 @@ Supported assets include: ### Creator Platform -* User registration -* Wallet authentication -* Artist profiles -* Portfolio showcase -* Artwork listings -* Client discovery +- User registration +- Wallet authentication +- Artist profiles +- Portfolio showcase +- Artwork listings +- Client discovery ### Marketplace -* Purchase digital artwork -* Stellar payments -* Basic commission requests +- Purchase digital artwork +- Stellar payments +- Basic commission requests ### Smart Contracts -* Escrow contracts -* Artist payouts -* Platform fees +- Escrow contracts +- Artist payouts +- Platform fees --- ## Phase 2 — Professional Marketplace -* Messaging system -* Reviews & ratings -* Milestone-based projects -* Team collaboration -* Advanced search & filtering -* Artist verification badges +- Messaging system +- Reviews & ratings +- Milestone-based projects +- Team collaboration +- Advanced search & filtering +- Artist verification badges --- ## Phase 3 — Creative Economy -* Talent recruitment portal -* Creative agencies support -* Subscription memberships -* Community competitions -* Creator grants -* Portfolio analytics +- Talent recruitment portal +- Creative agencies support +- Subscription memberships +- Community competitions +- Creator grants +- Portfolio analytics --- ## Phase 4 — Ecosystem Expansion -* NFT ownership certificates -* Creative licensing marketplace -* DAO governance -* AI-powered talent matching -* Cross-chain integrations -* Ecosystem funding programs +- NFT ownership certificates +- Creative licensing marketplace +- DAO governance +- AI-powered talent matching +- Cross-chain integrations +- Ecosystem funding programs --- @@ -308,15 +308,15 @@ Supported assets include: Lumora is fully open source and welcomes contributions from: -* Stellar developers -* Soroban smart contract engineers -* Backend developers -* Frontend developers -* UI/UX designers -* Product designers -* Technical writers -* Community moderators -* Artists and creative professionals +- Stellar developers +- Soroban smart contract engineers +- Backend developers +- Frontend developers +- UI/UX designers +- Product designers +- Technical writers +- Community moderators +- Artists and creative professionals Please see: diff --git a/app/admin/disputes/page.tsx b/app/admin/disputes/page.tsx index 6b4ecfc..ffdb427 100644 --- a/app/admin/disputes/page.tsx +++ b/app/admin/disputes/page.tsx @@ -58,7 +58,9 @@ function normalizeDispute(item: unknown, index?: number): DisputedCommission { email: String(artist.email ?? entry.artistEmail ?? ''), }, amount: Number(entry.amount ?? entry.totalAmount ?? entry.budgetUsdc ?? 0), - disputeReason: String(entry.disputeReason ?? entry.reason ?? entry.description ?? 'No reason provided'), + disputeReason: String( + entry.disputeReason ?? entry.reason ?? entry.description ?? 'No reason provided' + ), createdAt: String(entry.createdAt ?? entry.created_at ?? ''), }; } @@ -120,8 +122,8 @@ export default function AdminDisputesPage() { resolution: resolutionType, ...(resolutionType === 'partial' && { clientPercentage: splitPercentage, - artistPercentage: 100 - splitPercentage - }) + artistPercentage: 100 - splitPercentage, + }), }; const response = await fetch(`/admin/commissions/${selectedDispute.id}/resolve-dispute`, { @@ -134,7 +136,7 @@ export default function AdminDisputesPage() { if (response.ok) { // Remove the resolved dispute from the list - setDisputes(disputes.filter(d => d.id !== selectedDispute.id)); + setDisputes(disputes.filter((d) => d.id !== selectedDispute.id)); closeModal(); } } catch (error) { @@ -151,7 +153,7 @@ export default function AdminDisputesPage() {
- {[1, 2, 3].map(i => ( + {[1, 2, 3].map((i) => (
))}
@@ -166,7 +168,9 @@ export default function AdminDisputesPage() {

Dispute Management

-

Manage and resolve disputed commissions

+

+ Manage and resolve disputed commissions +

{error && ( @@ -191,33 +195,54 @@ export default function AdminDisputesPage() {
) : (
- {disputes.map(dispute => ( -
+ {disputes.map((dispute) => ( +
{/* Commission Title */}
-

Commission

+

+ Commission +

{dispute.title}

{/* Client */}
-

Client

-

{dispute.client.name}

-

{dispute.client.email}

+

+ Client +

+

+ {dispute.client.name} +

+

+ {dispute.client.email} +

{/* Artist */}
-

Artist

-

{dispute.artist.name}

-

{dispute.artist.email}

+

+ Artist +

+

+ {dispute.artist.name} +

+

+ {dispute.artist.email} +

{/* Amount */}
-

Amount

-

${dispute.amount.toFixed(2)}

+

+ Amount +

+

+ ${dispute.amount.toFixed(2)} +

{/* Resolve Button */} @@ -233,7 +258,9 @@ export default function AdminDisputesPage() { {/* Dispute Reason */}
-

Dispute Reason

+

+ Dispute Reason +

{dispute.disputeReason}

@@ -260,7 +287,9 @@ export default function AdminDisputesPage() { {selectedDispute && ( <>
-

{selectedDispute.title}

+

+ {selectedDispute.title} +

Amount: ${selectedDispute.amount.toFixed(2)}

@@ -268,7 +297,9 @@ export default function AdminDisputesPage() { {/* Resolution Type Selection */}
-

Resolution Type

+

+ Resolution Type +

@@ -309,11 +346,17 @@ export default function AdminDisputesPage() { {/* Partial Split Slider */} {resolutionType === 'partial' && (
-

Payment Split

+

+ Payment Split +

- Client: {splitPercentage}% - Artist: {100 - splitPercentage}% + + Client: {splitPercentage}% + + + Artist: {100 - splitPercentage}% +

- Client will receive: ${(selectedDispute.amount * splitPercentage / 100).toFixed(2)} + Client will receive: $ + {((selectedDispute.amount * splitPercentage) / 100).toFixed(2)}

- Artist will receive: ${(selectedDispute.amount * (100 - splitPercentage) / 100).toFixed(2)} + Artist will receive: $ + {((selectedDispute.amount * (100 - splitPercentage)) / 100).toFixed(2)}

@@ -355,4 +400,4 @@ export default function AdminDisputesPage() {
); -} \ No newline at end of file +} diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 31a9d15..5d8ddd2 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -21,14 +21,14 @@ export default function AdminPage() { const fetchAnalytics = async () => { setLoading(true); setError(''); - + try { const params = new URLSearchParams(); if (startDate) params.append('startDate', startDate); if (endDate) params.append('endDate', endDate); - + const response = await fetch(`/api/admin/analytics?${params.toString()}`); - + if (response.ok) { const data = await response.json(); setAnalytics(data); @@ -54,20 +54,28 @@ export default function AdminPage() { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', - minimumFractionDigits: 2 + minimumFractionDigits: 2, }).format(amount); }; - const KPICard = ({ title, value, icon, color }: { title: string; value: number | string; icon: React.ReactNode; color: string }) => ( + const KPICard = ({ + title, + value, + icon, + color, + }: { + title: string; + value: number | string; + icon: React.ReactNode; + color: string; + }) => (

{title}

{value}

-
- {icon} -
+
{icon}
); @@ -76,8 +84,10 @@ export default function AdminPage() {
-

Admin Dashboard

- +

+ Admin Dashboard +

+ {/* Date Range Picker */}
@@ -112,7 +122,10 @@ export default function AdminPage() { {loading ? (
{[...Array(5)].map((_, i) => ( -
+
@@ -125,8 +138,18 @@ export default function AdminPage() { value={analytics.totalUsers.toLocaleString()} color="bg-blue-100 dark:bg-blue-900/30" icon={ - - + + } /> @@ -136,8 +159,18 @@ export default function AdminPage() { value={analytics.activeArtists.toLocaleString()} color="bg-green-100 dark:bg-green-900/30" icon={ - - + + } /> @@ -147,8 +180,18 @@ export default function AdminPage() { value={analytics.commissionsThisWeek.toLocaleString()} color="bg-purple-100 dark:bg-purple-900/30" icon={ - - + + } /> @@ -158,8 +201,18 @@ export default function AdminPage() { value={formatUSDC(analytics.totalUSDCVolume)} color="bg-yellow-100 dark:bg-yellow-900/30" icon={ - - + + } /> @@ -169,8 +222,18 @@ export default function AdminPage() { value={analytics.openDisputes.toLocaleString()} color="bg-red-100 dark:bg-red-900/30" icon={ - - + + } /> @@ -179,4 +242,4 @@ export default function AdminPage() {
); -} \ No newline at end of file +} diff --git a/app/artists/[id]/components/PortfolioTab.tsx b/app/artists/[id]/components/PortfolioTab.tsx index 5701101..59921b8 100644 --- a/app/artists/[id]/components/PortfolioTab.tsx +++ b/app/artists/[id]/components/PortfolioTab.tsx @@ -24,7 +24,10 @@ export default function PortfolioTab({ artistId }: PortfolioTabProps) { return (
{[...Array(3)].map((_, i) => ( -
+
diff --git a/app/artists/[id]/components/ServicesTab.tsx b/app/artists/[id]/components/ServicesTab.tsx index b2fcf8e..522622e 100644 --- a/app/artists/[id]/components/ServicesTab.tsx +++ b/app/artists/[id]/components/ServicesTab.tsx @@ -24,7 +24,10 @@ export default function ServicesTab({ artistId }: ServicesTabProps) { return (
{[...Array(3)].map((_, i) => ( -
+
diff --git a/app/artists/[id]/page.tsx b/app/artists/[id]/page.tsx index 4d7d2df..ca5175f 100644 --- a/app/artists/[id]/page.tsx +++ b/app/artists/[id]/page.tsx @@ -10,7 +10,15 @@ import ServicesTab from './components/ServicesTab'; import ReviewsTab from './components/ReviewsTab'; import Button from '@/app/components/ui/Button'; import { Skeleton } from '@/app/components/ui/Skeleton'; -import { MapPin, Star, ShieldCheck, Briefcase, MessageSquare, Image as ImageIcon, Mail } from 'lucide-react'; +import { + MapPin, + Star, + ShieldCheck, + Briefcase, + MessageSquare, + Image as ImageIcon, + Mail, +} from 'lucide-react'; type TabKey = 'portfolio' | 'services' | 'reviews'; @@ -41,11 +49,17 @@ export default function ArtistProfilePage() {
- +
-

Artist Not Found

+

+ Artist Not Found +

- {error || "We couldn't find the artist you're looking for."} + {error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : "We couldn't find the artist you're looking for."}

@@ -54,7 +68,7 @@ export default function ArtistProfilePage() { const tabs: { key: TabKey; label: string; icon: React.ReactNode }[] = [ { key: 'portfolio', label: 'Portfolio', icon: }, - { key: 'services', label: 'Services', icon: }, + { key: 'services', label: 'Services', icon: }, { key: 'reviews', label: 'Reviews', icon: }, ]; @@ -78,7 +92,13 @@ export default function ArtistProfilePage() { {/* Avatar */}
{artist.avatar ? ( - {artist.name} + {artist.name} ) : (
@@ -100,7 +120,9 @@ export default function ArtistProfilePage() { )}
{artist.tagline && ( -

{artist.tagline}

+

+ {artist.tagline} +

)}
diff --git a/app/artists/[username]/page.tsx b/app/artists/[username]/page.tsx deleted file mode 100644 index 8985b16..0000000 --- a/app/artists/[username]/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Metadata } from "next"; - -type Props = { - params: { username: string }; -}; - -export async function generateMetadata({ params }: Props): Promise { - const username = params.username; - - return { - title: `${username} | Artist Profile | StellarAid`, - description: `View ${username}'s artist profile on StellarAid. Explore their portfolio of creative works and support their crowdfunding projects on the Stellar Network.`, - openGraph: { - title: `${username} | Artist Profile | StellarAid`, - description: `View ${username}'s artist profile on StellarAid. Explore their portfolio of creative works and support their crowdfunding projects on the Stellar Network.`, - images: ["/og-image.jpg"], - }, - }; -} - -export default function ArtistProfile({ params }: Props) { - return ( -
-

{params.username}'s Profile

-

Artist profile page

-
- ); -} \ No newline at end of file diff --git a/app/auth/register/page.tsx b/app/auth/register/page.tsx index 787adff..7d57e19 100644 --- a/app/auth/register/page.tsx +++ b/app/auth/register/page.tsx @@ -36,13 +36,15 @@ const RegisterPage = () => { return; } setLocalError(null); - dispatch(registerUser({ fullName, email, password })).then((result: any) => { - if (registerUser.fulfilled.match(result)) { - router.push('/auth/check-email'); - } - }).catch(() => { - // Handled by Redux authError state - }); + dispatch(registerUser({ fullName, email, password })) + .then((result: any) => { + if (registerUser.fulfilled.match(result)) { + router.push('/auth/check-email'); + } + }) + .catch(() => { + // Handled by Redux authError state + }); }; return ( diff --git a/app/commissions/[id]/page.tsx b/app/commissions/[id]/page.tsx index ff88651..4ebfbe3 100644 --- a/app/commissions/[id]/page.tsx +++ b/app/commissions/[id]/page.tsx @@ -2,24 +2,11 @@ import { useState } from 'react'; import Link from 'next/link'; -import { - Calendar, - Check, - Clock, - FileText, - PlusCircle, - Star, - X, -} from 'lucide-react'; +import { Calendar, Check, Clock, FileText, PlusCircle, Star, X } from 'lucide-react'; import toast from 'react-hot-toast'; import { generatePdf } from '@/lib/pdfExport'; -type CommissionStatus = - | 'PENDING' - | 'IN_PROGRESS' - | 'SUBMITTED' - | 'COMPLETED' - | 'CANCELLED'; +type CommissionStatus = 'PENDING' | 'IN_PROGRESS' | 'SUBMITTED' | 'COMPLETED' | 'CANCELLED'; type Role = 'artist' | 'client'; type Tab = 'overview' | 'milestones'; @@ -100,13 +87,11 @@ const STATUS_BADGE: Record toast.success(`${label} (mock)`); export default function CommissionDetailPage({ params }: { params: { id: string } }) { - const commission = - MOCK_COMMISSIONS.find((c) => c.id === params.id) ?? MOCK_COMMISSIONS[0]!; + const commission = MOCK_COMMISSIONS.find((c) => c.id === params.id) ?? MOCK_COMMISSIONS[0]!; const [role, setRole] = useState('client'); const [tab, setTab] = useState('overview'); @@ -137,14 +121,10 @@ export default function CommissionDetailPage({ params }: { params: { id: string .reduce((sum, m) => sum + m.amountUsdc, 0); const totalProposed = milestones.reduce((sum, m) => sum + m.amountUsdc, 0); const pct = - commission.budgetUsdc === 0 - ? 0 - : Math.round((approvedTotal / commission.budgetUsdc) * 100); + commission.budgetUsdc === 0 ? 0 : Math.round((approvedTotal / commission.budgetUsdc) * 100); const approveMilestone = (id: string) => { - setMilestones((prev) => - prev.map((m) => (m.id === id ? { ...m, approved: true } : m)), - ); + setMilestones((prev) => prev.map((m) => (m.id === id ? { ...m, approved: true } : m))); notice('Milestone approved'); }; @@ -167,7 +147,7 @@ export default function CommissionDetailPage({ params }: { params: { id: string const projected = totalProposed + amount; if (projected > commission.budgetUsdc) { setDraftError( - `Sum of milestones ($${projected} USDC) would exceed the commission budget ($${commission.budgetUsdc} USDC).`, + `Sum of milestones ($${projected} USDC) would exceed the commission budget ($${commission.budgetUsdc} USDC).` ); return; } @@ -193,10 +173,21 @@ export default function CommissionDetailPage({ params }: { params: { id: string filename: `commission-${commission.id}.pdf`, title: commission.title, sections: [ - { heading: 'Details', content: `Commission #${commission.id}\n${commission.client.name} → ${commission.artist.name}\nStatus: ${badge.label}\nDeadline: ${commission.deadline}` }, + { + heading: 'Details', + content: `Commission #${commission.id}\n${commission.client.name} → ${commission.artist.name}\nStatus: ${badge.label}\nDeadline: ${commission.deadline}`, + }, { heading: 'Budget', content: `$${commission.budgetUsdc.toFixed(2)} USDC` }, { heading: 'Description', content: commission.description }, - { heading: 'Milestones', content: milestones.map((m) => `${m.approved ? '✓' : '○'} ${m.title} — $${m.amountUsdc.toFixed(2)} USDC (due ${m.dueDate})`).join('\n') }, + { + heading: 'Milestones', + content: milestones + .map( + (m) => + `${m.approved ? '✓' : '○'} ${m.title} — $${m.amountUsdc.toFixed(2)} USDC (due ${m.dueDate})` + ) + .join('\n'), + }, ], }); }; @@ -298,8 +289,7 @@ export default function CommissionDetailPage({ params }: { params: { id: string {commission.title}

- Commission #{commission.id} · {commission.client.name} →{' '} - {commission.artist.name} + Commission #{commission.id} · {commission.client.name} → {commission.artist.name}

-