195 lines
5.6 KiB
TypeScript
195 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useEffect,
|
|
useRef,
|
|
type ReactNode,
|
|
} from 'react';
|
|
|
|
// Types
|
|
interface FrameworkData {
|
|
id: string;
|
|
name: string;
|
|
logo: string;
|
|
logoDark?: string;
|
|
}
|
|
|
|
interface QuickstartContextValue {
|
|
selectedId: string;
|
|
setSelectedId: (id: string) => void;
|
|
registerFramework: (data: FrameworkData) => void;
|
|
frameworks: FrameworkData[];
|
|
}
|
|
|
|
// Context to share state between QuickstartFlow and FrameworkOption
|
|
const QuickstartContext = createContext<QuickstartContextValue | null>(null);
|
|
|
|
export function useQuickstartFramework() {
|
|
const context = useContext(QuickstartContext);
|
|
return context?.selectedId ?? null;
|
|
}
|
|
|
|
// Framework option component - registers itself and conditionally renders content
|
|
interface FrameworkOptionProps {
|
|
id: string;
|
|
name: string;
|
|
logo: string;
|
|
logoDark?: string;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function FrameworkOption({ id, name, logo, logoDark, children }: FrameworkOptionProps) {
|
|
const context = useContext(QuickstartContext);
|
|
const hasRegistered = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (context && !hasRegistered.current) {
|
|
context.registerFramework({ id, name, logo, logoDark });
|
|
hasRegistered.current = true;
|
|
}
|
|
}, [context, id, name, logo, logoDark]);
|
|
|
|
// Only render if this framework is selected
|
|
if (!context || context.selectedId !== id) {
|
|
return null;
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|
|
|
|
// Card component for framework selection
|
|
interface FrameworkCardProps {
|
|
id: string;
|
|
name: string;
|
|
logo: string;
|
|
logoDark?: string;
|
|
selected: boolean;
|
|
onSelect: () => void;
|
|
}
|
|
|
|
function FrameworkCard({ name, logo, logoDark, selected, onSelect }: FrameworkCardProps) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onSelect}
|
|
className={`
|
|
flex items-center gap-3 rounded-lg border px-3 py-2.5 text-left transition-all
|
|
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
|
|
${selected
|
|
? 'border-[var(--composio-orange)] bg-fd-accent/30'
|
|
: 'border-fd-border bg-fd-card hover:bg-fd-accent/50 hover:border-[color-mix(in_srgb,var(--composio-orange)_50%,transparent)]'
|
|
}
|
|
`}
|
|
>
|
|
<div className="flex h-6 w-6 shrink-0 items-center justify-center">
|
|
{logoDark ? (
|
|
<>
|
|
<Image
|
|
src={logo}
|
|
alt={`${name} logo`}
|
|
width={24}
|
|
height={24}
|
|
className="h-5 w-auto dark:hidden"
|
|
/>
|
|
<Image
|
|
src={logoDark}
|
|
alt={`${name} logo`}
|
|
width={24}
|
|
height={24}
|
|
className="h-5 w-auto hidden dark:block"
|
|
/>
|
|
</>
|
|
) : (
|
|
<Image
|
|
src={logo}
|
|
alt={`${name} logo`}
|
|
width={24}
|
|
height={24}
|
|
className="h-5 w-auto"
|
|
/>
|
|
)}
|
|
</div>
|
|
<span className="text-sm font-medium text-fd-foreground">{name}</span>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
// Main wrapper component
|
|
interface QuickstartFlowProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function QuickstartFlow({ children }: QuickstartFlowProps) {
|
|
const [frameworks, setFrameworks] = useState<FrameworkData[]>([]);
|
|
const [selectedId, setSelectedId] = useState<string>('');
|
|
const registeredIds = useRef<Set<string>>(new Set());
|
|
|
|
const registerFramework = (data: FrameworkData) => {
|
|
if (!registeredIds.current.has(data.id)) {
|
|
registeredIds.current.add(data.id);
|
|
setFrameworks((prev) => {
|
|
// Check if already exists
|
|
if (prev.some((f) => f.id === data.id)) {
|
|
return prev;
|
|
}
|
|
return [...prev, data];
|
|
});
|
|
}
|
|
};
|
|
|
|
// Set default selection to first framework once registered
|
|
useEffect(() => {
|
|
if (frameworks.length > 0 && !selectedId) {
|
|
setSelectedId(frameworks[0].id);
|
|
}
|
|
}, [frameworks, selectedId]);
|
|
|
|
const contextValue: QuickstartContextValue = {
|
|
selectedId,
|
|
setSelectedId,
|
|
registerFramework,
|
|
frameworks,
|
|
};
|
|
|
|
return (
|
|
<QuickstartContext.Provider value={contextValue}>
|
|
{/* Framework + integration type selector */}
|
|
{frameworks.length > 0 && (
|
|
<div className="not-prose mb-6 rounded-lg border border-fd-border bg-fd-card/50 p-4">
|
|
<p className="mb-3 text-sm font-medium text-fd-muted-foreground">Choose your framework</p>
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
{frameworks.map((framework) => (
|
|
<FrameworkCard
|
|
key={framework.id}
|
|
id={framework.id}
|
|
name={framework.name}
|
|
logo={framework.logo}
|
|
logoDark={framework.logoDark}
|
|
selected={selectedId === framework.id}
|
|
onSelect={() => setSelectedId(framework.id)}
|
|
/>
|
|
))}
|
|
<Link
|
|
href="/docs/providers"
|
|
className="flex items-center gap-3 rounded-lg border border-dashed border-fd-border px-3 py-2.5 transition-all hover:bg-fd-accent/50 hover:border-fd-muted-foreground"
|
|
>
|
|
<span className="flex h-6 w-6 shrink-0 items-center justify-center text-fd-muted-foreground">→</span>
|
|
<span className="text-sm font-medium text-fd-muted-foreground">Other providers</span>
|
|
</Link>
|
|
</div>
|
|
{/* Portal target for integration tabs header */}
|
|
<div id="integration-tabs-portal" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Render all children - FrameworkOption will conditionally show based on selection */}
|
|
{children}
|
|
</QuickstartContext.Provider>
|
|
);
|
|
}
|