nomadweb/frontend/src/components/GigsClient.tsx
eric e23cf9a03c Close Explore/Grow loops: visa wizard, jobs, lessons, gigs notify, map.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 09:26:56 -05:00

193 lines
6.3 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { api } from "@/lib/api";
import { useAuth } from "@/lib/auth";
import { useToast } from "@/lib/toast";
import { useI18n } from "@/lib/i18n";
import type { GigItem } from "@/lib/types";
import RingNext from "@/components/RingNext";
import { useRingSteps } from "@/lib/rings";
import {
clearApplyDraft,
clearSavedGigs,
loadApplyDrafts,
loadSavedGigs,
saveApplyDraft,
toggleSavedGig,
type SavedGig,
} from "@/lib/savedGigs";
export default function GigsClient({ gigs }: { gigs: GigItem[] }) {
const { token } = useAuth();
const router = useRouter();
const { toast } = useToast();
const { t } = useI18n();
const rings = useRingSteps();
const [msg, setMsg] = useState<Record<string, string>>({});
const [applied, setApplied] = useState<Record<string, boolean>>({});
const [busyId, setBusyId] = useState<string | null>(null);
const [saved, setSaved] = useState<SavedGig[]>([]);
useEffect(() => {
setSaved(loadSavedGigs());
setMsg(loadApplyDrafts());
}, []);
const onToggleSave = (g: GigItem) => {
const next = toggleSavedGig({
id: g.id,
title: g.title,
budget: g.budget,
deadline: g.deadline,
});
const now = next.some((x) => x.id === g.id);
setSaved(next);
toast(now ? t.gigs.saved : t.gigs.unsaved, "success");
};
const apply = async (id: string) => {
if (!token) {
router.push("/login?next=/gigs");
return;
}
const message = msg[id]?.trim();
if (!message) {
toast(t.gigs.applyNeed, "info");
return;
}
setBusyId(id);
try {
await api.applyGig(token, id, message);
setApplied((a) => ({ ...a, [id]: true }));
clearApplyDraft(id);
setMsg((m) => {
const next = { ...m };
delete next[id];
return next;
});
toast(t.gigs.applyOk, "success", {
href: "/notifications",
label: t.nav.notifications,
});
} catch {
toast(t.gigs.applyFail, "error");
} finally {
setBusyId(null);
}
};
return (
<div className="gigs-page">
<div className="container">
<nav className="detail-nav"><Link href="/digital">← {t.nav.digital}</Link></nav>
<div className="section-header reveal">
<span className="section-tag">{t.gigs.tag}</span>
<h1>{t.gigs.title}</h1>
<p>{t.gigs.subtitle}</p>
<Link href="/gigs/post" className="btn btn-primary btn-sm">{t.gigs.postTitle}</Link>
</div>
{saved.length > 0 && (
<section className="saved-gigs-panel reveal">
<div className="saved-gigs-head">
<h2>
{t.gigs.savedList} · {saved.length}
</h2>
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => {
clearSavedGigs();
setSaved([]);
}}
>
{t.gigs.clearSaved}
</button>
</div>
<div className="saved-gigs-list">
{saved.map((g) => (
<a key={g.id} href={`#gig-${g.id}`} className="saved-gig-chip">
<strong>{g.title}</strong>
<small>
{g.budget} · {g.deadline}
</small>
</a>
))}
</div>
</section>
)}
{gigs.length === 0 ? (
<div className="gigs-empty reveal">
<p className="dest-empty">{t.gigs.empty}</p>
<p className="section-desc">{t.gigs.emptyHint}</p>
<div className="dating-empty-actions">
<Link href="/gigs/post" className="btn btn-primary btn-sm">{t.gigs.postTitle}</Link>
<Link href="/digital" className="btn btn-sm">{t.nav.digital}</Link>
</div>
</div>
) : (
<div className="digital-jobs-grid reveal">
{!token && (
<p className="section-desc reveal">
<Link href="/login?next=/gigs">{t.nav.login}</Link> · {t.gigs.loginFirst}
</p>
)}
{gigs.map((g) => {
const isSaved = saved.some((s) => s.id === g.id);
return (
<article key={g.id} id={`gig-${g.id}`} className="digital-job-card">
<div className="gig-card-top">
<h3>{g.title}</h3>
<button
type="button"
className={`btn btn-sm${isSaved ? " btn-primary" : " btn-ghost"}`}
onClick={() => onToggleSave(g)}
>
{isSaved ? `★ ${t.gigs.saved}` : `☆ ${t.gigs.save}`}
</button>
</div>
<p>{g.description}</p>
<p className="digital-job-meta">{g.poster} · {g.budget} · {g.deadline}</p>
{applied[g.id] ? (
<p className="gigs-applied">{t.gigs.applied}</p>
) : !token ? (
<Link href="/login?next=/gigs" className="btn btn-primary btn-sm">
{t.gigs.apply}
</Link>
) : (
<>
<textarea
rows={2}
placeholder={t.gigs.applyPlaceholder}
value={msg[g.id] || ""}
onChange={(e) => {
const value = e.target.value;
setMsg({ ...msg, [g.id]: value });
saveApplyDraft(g.id, value);
}}
/>
<button
type="button"
className="btn btn-primary btn-sm"
disabled={busyId === g.id}
onClick={() => void apply(g.id)}
>
{busyId === g.id ? t.gigs.applying : t.gigs.apply}
</button>
</>
)}
</article>
);
})}
</div>
)}
<RingNext steps={rings.afterGigs} />
</div>
</div>
);
}