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
228 changes: 91 additions & 137 deletions apps/web/app/jobs/[id]/page.tsx

Large diffs are not rendered by default.

64 changes: 34 additions & 30 deletions apps/web/app/jobs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,33 @@ function JobCard({ job }: { job: BoardJob }) {
);
}

function SkeletonGrid() {
return (
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<JobCardSkeleton key={i} />
))}
</div>
);
}

function StatsBar({ total, filtered }: { total: number; filtered: number }) {
return (
<div className="flex items-center gap-3 text-xs font-medium text-zinc-500">
<div className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
<span>{total} Total Listings</span>
</div>
{filtered !== total && (
<div className="flex items-center gap-1.5">
<span className="h-1.5 w-1.5 rounded-full bg-zinc-700" />
<span>{filtered} Matched</span>
</div>
)}
</div>
);
}

// ─── Page ────────────────────────────────────────────────────────────────────

export default function JobsPage() {
Expand Down Expand Up @@ -291,7 +318,7 @@ export default function JobsPage() {
</div>
</div>

{/* ── Filter & sort bar ────────────────────────────────────────────── */}
{/* Filter & sort bar */}
<JobFilters
query={query}
setQuery={actions.setQuery}
Expand All @@ -308,7 +335,7 @@ export default function JobsPage() {
setFilterStatus={actions.setFilterStatus}
/>

{/* ── Error banner ─────────────────────────────────────────────────── */}
{/* Error banner */}
{error && (
<div
role="alert"
Expand All @@ -322,7 +349,7 @@ export default function JobsPage() {
</div>
)}

{/* ── Results header ───────────────────────────────────────────────── */}
{/* Results header */}
{!loading && (
<div className="flex items-center justify-between gap-4">
<StatsBar total={totalOpen} filtered={paginatedJobs.length} />
Expand All @@ -338,7 +365,7 @@ export default function JobsPage() {
</div>
)}

{/* ── Job grid ─────────────────────────────────────────────────────── */}
{/* Job grid */}
<main aria-label="Job listings">
{loading ? (
<SkeletonGrid />
Expand Down Expand Up @@ -371,7 +398,7 @@ export default function JobsPage() {
)}
</main>

{/* ── Bottom CTA ───────────────────────────────────────────────────── */}
{/* Bottom CTA */}
{!loading && paginatedJobs.length > 0 && (
<footer className="relative overflow-hidden rounded-3xl border border-zinc-800/80 bg-zinc-900/60 p-6 backdrop-blur-sm sm:p-8">
<div
Expand All @@ -398,31 +425,8 @@ export default function JobsPage() {
</div>
)}
</div>

<main>
{loading ? (
<div className="grid gap-4 sm:grid-cols-2">
{Array.from({ length: 4 }).map((_, i) => (
<JobCardSkeleton key={i} />
))}
</div>
) : jobs.length > 0 ? (
<div className="grid gap-4 sm:grid-cols-2">
{jobs.map((job) => (
<JobCard key={job.id} job={job} />
))}
</div>
) : (
<EmptyState
tone="dark"
icon={<Briefcase className="h-5 w-5" />}
title="No matches found"
description="Adjust your filters to discover more open opportunities."
/>
)}
</main>
</div>
</div>
</footer>
)}
</div>
);
}
30 changes: 15 additions & 15 deletions apps/web/components/blockchain/sign-transaction-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useEffect, useState, useMemo } from "react";
import {
ShieldCheck,
X,
Expand All @@ -26,25 +26,25 @@ interface SignTransactionModalProps {
}

export function SignTransactionModal({ xdr, onConfirm, onCancel }: SignTransactionModalProps) {
const [decoded, setDecoded] = useState<DecodedTransaction | null>(null);
const [isVisible, setIsVisible] = useState(false);
const { network } = useWalletStore();
const decoded = useMemo(() => {
if (!xdr) return null;
try {
return decodeTransaction(xdr);
} catch (err) {
console.error("Failed to decode XDR:", err);
return null;
}
}, [xdr]);

useEffect(() => {
if (xdr) {
try {
const decodedTx = decodeTransaction(xdr);
setDecoded(decodedTx);
// Small delay to trigger entry animation
const timer = setTimeout(() => setIsVisible(true), 10);
return () => clearTimeout(timer);
} catch (err) {
console.error("Failed to decode XDR:", err);
}
if (xdr && decoded) {
// Small delay to trigger entry animation
const timer = setTimeout(() => setIsVisible(true), 10);
return () => clearTimeout(timer);
} else {
setIsVisible(false);
}
}, [xdr]);
}, [xdr, decoded]);

if (!xdr || !decoded) return null;

Expand Down
1 change: 1 addition & 0 deletions apps/web/components/jobs/accept-bid-flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface AcceptBidFlowProps {
job: Job;
bids: Bid[];
isClientOwner: boolean;
onAccept?: () => void;
onSuccess?: () => void;
children: (props: {
handleAcceptClick: (bidId: string) => void;
Expand Down
46 changes: 3 additions & 43 deletions apps/web/components/jobs/bid-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,26 +88,16 @@ interface BidListProps {
loading?: boolean;
error?: string | null;
isClientOwner?: boolean;
onSuccess?: () => void;
}

/**
* BidList — Issue #132 & #135
*
* Renders the list of bids on a job from the client's perspective.
* - Shows loading skeletons while bids are being fetched
* - Empty state when no bids have been submitted
* - Error boundary fallback for fetch failures
* - Per-bid "Accept" action for the client owner on open jobs
* - Status badges with semantic colour coding (Amber = pending, Emerald = accepted)
* - Fully responsive with keyboard-accessible accept buttons
* - Integrated "Accept Bid" flow with confirmation modal
*/
export function BidList({
job,
bids,
loading = false,
error = null,
isClientOwner = false,
onSuccess,
}: BidListProps) {
const [expandedId, setExpandedId] = useState<string | null>(null);

Expand All @@ -129,7 +119,7 @@ export function BidList({
const canAccept = isClientOwner && job.status === "open";

return (
<AcceptBidFlow job={job} bids={bids} isClientOwner={isClientOwner}>
<AcceptBidFlow job={job} bids={bids} isClientOwner={isClientOwner} onSuccess={onSuccess}>
{({ handleAcceptClick, acceptingBidId }) => (
<ul aria-label="Bids" className="space-y-3">
{bids.map((bid) => {
Expand Down Expand Up @@ -197,36 +187,6 @@ export function BidList({
</button>
)}

{/* Accept action */}
{canAccept && !isAccepted && (
<div className="mt-4 flex justify-end">
<Button
size="sm"
onClick={() => handleAcceptClick(bid.id)}
disabled={isAccepting || Boolean(acceptingBidId)}
aria-label={`Accept bid from ${shortenAddress(bid.freelancer_address)}`}
aria-busy={isAccepting}
className="rounded-full bg-emerald-600 text-xs font-medium text-white shadow-sm shadow-emerald-500/20 transition-all duration-150 hover:bg-emerald-500 focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900 disabled:opacity-60"
>
{isAccepting ? (
<>
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden="true" />
Accepting…
</>
) : (
<span className="text-zinc-500">No reputation yet</span>
)}
</Button>
</div>
)}

{isAccepted && (
<p className="mt-3 flex items-center gap-1.5 text-xs font-medium text-emerald-400">
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
Bid accepted — work in progress
</p>
)}

{/* Accept action */}
{canAccept && !isAccepted && (
<div className="mt-4 flex justify-end">
Expand Down
16 changes: 0 additions & 16 deletions apps/web/components/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@ import { ThemeProvider } from "next-themes";
import React, { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AuthBootstrap } from "@/components/state/auth-bootstrap";
<<<<<<< feat/job-search-ui
import { getQueryClient } from "@/lib/query-client";
import { TransactionSigningProvider } from "@/components/blockchain/transaction-signing-provider";
=======
>>>>>>> main

export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());

return (
<QueryClientProvider client={queryClient}>
<ThemeProvider
<<<<<<< feat/job-search-ui
attribute="class"
defaultTheme="system"
enableSystem
Expand All @@ -30,16 +25,5 @@ export function Providers({ children }: { children: React.ReactNode }) {
</AuthBootstrap>
</ThemeProvider>
</QueryClientProvider>
=======
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
storageKey="lance-theme"
>
<AuthBootstrap>{children}</AuthBootstrap>
</ThemeProvider>
</QueryClientProvider>
>>>>>>> main
);
}
1 change: 0 additions & 1 deletion apps/web/components/wallet/wallet-error-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { AlertTriangle, Info, RefreshCw, X } from "lucide-react";
import { Button } from "@/components/ui/button";

import { cn } from "@/lib/utils";

interface WalletErrorDisplayProps {
Expand Down
Loading