193 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
}
|