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>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user