0ffdc362b5
- 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>
63 lines
1.6 KiB
TypeScript
63 lines
1.6 KiB
TypeScript
'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>
|
|
);
|
|
}
|