feat: member portal Sprint 4 — Events + RSVP
- Event + EventRsvp models + migration (RsvpStatus enum: GOING/MAYBE/NOT_GOING) - EventsModule: admin CRUD (create/update/delete/publish) + RSVP list - GET /my/events + POST /my/events/:id/rsvp in MyController/MyService - Admin BFF routes: GET/POST /api/admin/events, PATCH/DELETE /api/admin/events/[id] - Member BFF routes: GET /api/my/events, POST /api/my/events/[id]/rsvp - /my/events page: upcoming/past split, RSVP button (client component, optimistic) - Register DigestModule, OrgModule, ThreadsModule, EventsModule in AppModule - Add EVENT_CREATED/DELETED/UPDATED and DIGEST_SENT to AuditAction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
type RsvpStatus = 'GOING' | 'NOT_GOING' | 'MAYBE';
|
||||
|
||||
const LABELS: Record<RsvpStatus, string> = {
|
||||
GOING: 'Going',
|
||||
MAYBE: 'Maybe',
|
||||
NOT_GOING: 'Not going',
|
||||
};
|
||||
|
||||
const STYLES: Record<RsvpStatus, string> = {
|
||||
GOING: 'bg-green-600 text-white hover:bg-green-700',
|
||||
MAYBE: 'bg-yellow-500 text-white hover:bg-yellow-600',
|
||||
NOT_GOING: 'bg-gray-200 text-gray-700 hover:bg-gray-300',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
eventId: string;
|
||||
initial: RsvpStatus | null;
|
||||
}
|
||||
|
||||
export function RsvpButton({ eventId, initial }: Props) {
|
||||
const [current, setCurrent] = useState<RsvpStatus | null>(initial);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const rsvp = async (status: RsvpStatus) => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/my/events/${eventId}/rsvp`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status }),
|
||||
});
|
||||
if (res.ok) setCurrent(status);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 mt-3">
|
||||
{(['GOING', 'MAYBE', 'NOT_GOING'] as RsvpStatus[]).map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
disabled={loading}
|
||||
onClick={() => rsvp(s)}
|
||||
className={`text-xs rounded-full px-3 py-1.5 font-medium transition-colors disabled:opacity-50 border ${
|
||||
current === s
|
||||
? STYLES[s]
|
||||
: 'border-gray-200 text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
{LABELS[s]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user