Skip to main content
Glama
page.tsx9.9 kB
'use client'; import { useAuth, useOrganizationList, useUser, CreateOrganization, UserButton } from '@clerk/nextjs'; import { useSearchParams, useRouter } from 'next/navigation'; import { useState, Suspense, useEffect, useRef } from 'react'; import { Col } from '@/components/col' import { Row } from '@/components/row' import { LoadingState } from '@/components/spinner/loading-state'; function SelectOrgContent(): React.ReactElement { const { isLoaded, setActive, userMemberships } = useOrganizationList({ userMemberships: { infinite: true, }, }); const { orgId } = useAuth(); const { user } = useUser(); const searchParams = useSearchParams(); const router = useRouter(); const [isSelecting, setIsSelecting] = useState(false); const [selectedOrgId, setSelectedOrgId] = useState<string | null>(orgId || null); const [canScrollUp, setCanScrollUp] = useState(false); const [canScrollDown, setCanScrollDown] = useState(false); const scrollContainerRef = useRef<HTMLDivElement>(null); // Check if we just returned from org creation and reload to get fresh data useEffect(() => { if (searchParams.get('org_created') === 'true') { // Remove the flag from URL and reload to get fresh organization data const newUrl = new URL(window.location.href); newUrl.searchParams.delete('org_created'); window.location.href = newUrl.toString(); } }, [searchParams]); // Check scroll state const updateScrollState = (): void => { const container = scrollContainerRef.current; if (!container) return; const { scrollTop, scrollHeight, clientHeight } = container; setCanScrollUp(scrollTop > 0); setCanScrollDown(scrollTop < scrollHeight - clientHeight); }; // Update scroll state when content changes useEffect(() => { updateScrollState(); }, [userMemberships?.data]); // Get the original OAuth parameters from the URL const originalParams = { client_id: searchParams.get('client_id'), redirect_uri: searchParams.get('redirect_uri'), response_type: searchParams.get('response_type'), scope: searchParams.get('scope'), state: searchParams.get('state'), code_challenge: searchParams.get('code_challenge'), code_challenge_method: searchParams.get('code_challenge_method'), }; const handleOrgSelect = (organizationId: string): void => { setSelectedOrgId(organizationId); }; const handleConfirm = async (): Promise<void> => { if (!setActive || isSelecting || !selectedOrgId) return; setIsSelecting(true); try { await setActive({ organization: selectedOrgId }); // After setting active org, redirect back to authorize const authorizeUrl = new URL('/authorize', window.location.origin); // Add all original OAuth parameters Object.entries(originalParams).forEach(([key, value]) => { if (value) authorizeUrl.searchParams.set(key, value); }); // Add the selected orgId as a parameter authorizeUrl.searchParams.set('org_id', selectedOrgId); const redirectUri = authorizeUrl.toString(); router.push(redirectUri); } catch (error) { console.error('Failed to set active organization:', error); setIsSelecting(false); } }; if (!isLoaded || userMemberships?.isLoading) { return ( <Col className="min-h-screen items-center justify-center"> <Col className="text-center items-center gap-2"> <LoadingState /> <p className="text-muted-foreground">Loading your organizations...</p> </Col> </Col> ); } // Check if user has any organizations (only after loaded) if (isLoaded && !userMemberships?.isLoading && (!userMemberships?.data || userMemberships.data.length === 0)) { return ( <Col className="min-h-screen items-center justify-center"> {/* User button in top-right corner */} <div className="absolute top-4 right-4"> <UserButton afterSignOutUrl={`/select-org?${searchParams.toString()}`} /> </div> <Col className="text-center max-w-md mx-auto p-6 gap-6"> <h1 className="text-2xl font-bold text-foreground">Create Organization</h1> <p className="text-muted-foreground"> You need to be a member of at least one organization to continue with authorization. </p> <CreateOrganization afterCreateOrganizationUrl={(() => { const params = new URLSearchParams(searchParams.toString()); params.set('org_created', 'true'); return `/select-org?${params.toString()}`; })()} skipInvitationScreen={true} /> </Col> </Col> ); } return ( <Col className="min-h-screen items-center justify-center"> {/* User button in top-right corner */} <div className="absolute top-4 right-4"> <UserButton afterSignOutUrl={`/select-org?${searchParams.toString()}`} /> </div> <Col className="max-w-md w-full mx-auto p-6 bg-muted rounded-lg gap-6"> <Col className="text-center gap-2"> <h1 className="text-2xl font-bold text-foreground">Select Organization</h1> <p className="text-muted-foreground"> Choose which organization you'd like to authorize access for. </p> {orgId && ( <p className="text-sm text-primary"> Currently active: {userMemberships?.data?.find(m => m.organization.id === orgId)?.organization.name || user?.organizationMemberships?.find(m => m.organization.id === orgId)?.organization.name || orgId} </p> )} </Col> <div className="relative"> <div ref={scrollContainerRef} onScroll={updateScrollState} className="flex flex-col gap-3 max-h-60 overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent" > {(userMemberships?.data || user?.organizationMemberships) ?.sort((a, b) => { // Put the currently active org first if (a.organization.id === orgId) return -1; if (b.organization.id === orgId) return 1; return 0; }) ?.map((membership) => { const isSelected = membership.organization.id === selectedOrgId; const isCurrentlyActive = membership.organization.id === orgId; return ( <button key={membership.organization.id} onClick={() => handleOrgSelect(membership.organization.id)} disabled={isSelecting} className={`w-full p-4 text-left border rounded-lg transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed ${ isSelected ? 'border-primary bg-background' : 'border-border hover:border-primary/50 hover:bg-accent/50' }`} > <Row className="gap-3"> {membership.organization.imageUrl && ( <img src={membership.organization.imageUrl} alt={membership.organization.name} className="w-10 h-10 rounded-full" /> )} <Col className="flex-1 gap-1"> <Row className="justify-between items-center"> <h3 className="font-medium text-foreground">{membership.organization.name}</h3> {isCurrentlyActive && ( <span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded-full"> Active </span> )} </Row> <p className="text-sm text-muted-foreground">{membership.organization.slug}</p> </Col> </Row> </button> ); })} </div> {/* Fade overlays to indicate scrollability */} {canScrollUp && ( <div className="absolute top-0 left-0 right-0 h-4 bg-gradient-to-b from-muted to-transparent pointer-events-none" /> )} {canScrollDown && ( <div className="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-t from-muted to-transparent pointer-events-none" /> )} </div> <Col className="gap-4"> <button onClick={handleConfirm} disabled={isSelecting || !selectedOrgId} className="w-full bg-primary text-primary-foreground py-3 px-4 rounded-lg font-medium hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-opacity" > {isSelecting ? ( <Row className="items-center justify-center gap-2"> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-foreground"></div> Setting Organization </Row> ) : 'Continue with Selected Organization'} </button> </Col> </Col> </Col> ); } function LoadingFallback(): React.ReactElement { return ( <Col className="min-h-screen items-center justify-center"> <Col className="text-center gap-2"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div> <p className="text-muted-foreground">Loading...</p> </Col> </Col> ); } export default function SelectOrgPage(): React.ReactElement { return ( <Suspense fallback={<LoadingFallback />}> <SelectOrgContent /> </Suspense> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/onkernel/kernel-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server