cd1eef0098
- 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>
60 lines
1.7 KiB
TypeScript
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>
|
|
);
|
|
}
|