Files
tower/apps/web/app/my/seva/SevaSignupButton.tsx
T
maaz519 cd1eef0098 feat: member portal Sprint 6 — Seva + multi-signal gamification
- SevaOpportunity + SevaEntry + GamificationEvent models + migration
- GamificationService: multi-signal point table (helpful answer +10, seva +20,
  event +5, FAQ +15, welcome +3, profile +5, business +5), idempotent award via
  unique(userId,type,refId), lifetime + time-decayed "recent" score, leaderboard
- SevaModule: admin CRUD + mark-entry-complete (awards SEVA_COMPLETED points)
- Member endpoints in MyController: GET /my/seva, POST /my/seva/:id/signup|cancel
- PROFILE_COMPLETED auto-awarded on profile update (name+hometown+interest)
- /my/seva page: points hero with per-type breakdown, open opportunities w/ signup,
  leaderboard (respects directoryVisible), my seva history
- Member + admin BFF routes; Seva & Points nav item
- Register GamificationModule + SevaModule; add SEVA_* audit actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 16:44:43 +05:30

60 lines
1.7 KiB
TypeScript

'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
type EntryStatus = 'SIGNED_UP' | 'COMPLETED' | 'CANCELLED' | null;
interface Props {
opportunityId: string;
initialStatus: EntryStatus;
full: boolean;
}
export function SevaSignupButton({ opportunityId, initialStatus, full }: Props) {
const router = useRouter();
const [status, setStatus] = useState<EntryStatus>(initialStatus);
const [loading, setLoading] = useState(false);
const act = async (action: 'signup' | 'cancel') => {
setLoading(true);
try {
const res = await fetch(`/api/my/seva/${opportunityId}/${action}`, { method: 'POST' });
if (res.ok) {
setStatus(action === 'signup' ? 'SIGNED_UP' : 'CANCELLED');
router.refresh();
}
} finally {
setLoading(false);
}
};
if (status === 'COMPLETED') {
return <span className="text-xs bg-green-100 text-green-700 rounded-full px-3 py-1.5 font-medium"> Completed</span>;
}
if (status === 'SIGNED_UP') {
return (
<button
type="button"
disabled={loading}
onClick={() => act('cancel')}
className="text-xs rounded-full px-3 py-1.5 font-medium border border-gray-200 text-gray-600 hover:bg-gray-50 disabled:opacity-50"
>
{loading ? '…' : 'Signed up · Cancel'}
</button>
);
}
return (
<button
type="button"
disabled={loading || full}
onClick={() => act('signup')}
className="text-xs rounded-full px-4 py-1.5 font-medium bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{full ? 'Full' : loading ? '…' : 'Sign up'}
</button>
);
}