feat: member portal Sprint 7 — Circles (interest/affinity sub-groups)
- Circle + CircleMembership models + migration (CircleRole: MEMBER/LEAD, public/invite-only) - CirclesModule: admin CRUD + member list; unique circle name per tenant - Member endpoints in MyController: GET /my/circles, POST /my/circles/:id/join|leave - listForMember returns public circles + private ones the member belongs to, with isMember/myRole/memberCount - join() enforces invite-only; leave() removes membership - /my/circles page: "My circles" + "Discover" split, join/leave button, LEAD + Private badges - Member + admin BFF routes; Circles nav item - Register CirclesModule; add CIRCLE_CREATED/DELETED audit actions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
circleId: string;
|
||||
initialMember: boolean;
|
||||
isPublic: boolean;
|
||||
}
|
||||
|
||||
export function CircleJoinButton({ circleId, initialMember, isPublic }: Props) {
|
||||
const router = useRouter();
|
||||
const [member, setMember] = useState(initialMember);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const act = async (action: 'join' | 'leave') => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/my/circles/${circleId}/${action}`, { method: 'POST' });
|
||||
if (res.ok) {
|
||||
setMember(action === 'join');
|
||||
router.refresh();
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (member) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
onClick={() => act('leave')}
|
||||
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 ? '…' : 'Joined · Leave'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading || !isPublic}
|
||||
onClick={() => act('join')}
|
||||
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"
|
||||
>
|
||||
{!isPublic ? 'Invite only' : loading ? '…' : 'Join'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user