57fe9d9bf1
- 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>
54 lines
1.4 KiB
TypeScript
54 lines
1.4 KiB
TypeScript
'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>
|
|
);
|
|
}
|