import dynamic from 'next/dynamic'
import {
createContext,
ElementRef,
PropsWithChildren,
useContext,
useEffect,
useRef,
useState,
} from 'react'
import { CommandInput } from './Command.utils'
// `CommandMenu` is heavy - code split to reduce app bundle size
const CommandMenu = dynamic(() => import('./CommandMenu'), {
loading: () =>
Loading...
,
})
export interface CommandMenuContextValue {
isOpen: boolean
setIsOpen: React.Dispatch>
isLoading: boolean
setIsLoading: React.Dispatch>
search: string
setSearch: React.Dispatch>
pages: string[]
setPages: React.Dispatch>
currentPage?: string
inputRef: React.RefObject
site: 'studio' | 'docs' | 'website'
/**
* Project metadata for easy retrieval
*/
project?: { ref?: string; apiKeys?: { anon?: string; service?: string }; apiUrl?: string }
/**
* Any additional metadata that CMDK component can use in its AI prompts
*/
metadata?: { definitions?: string; flags?: { [key: string]: string } }
/**
* Opt in flag to use additional metadata in AI prompts
*/
isOptedInToAI: boolean
// Optional callback to save a generated SQL output
saveGeneratedSQL?: (answer: string, title: string) => Promise
}
export const CommandMenuContext = createContext(undefined)
export const useCommandMenu = () => {
const context = useContext(CommandMenuContext)
if (context === undefined) {
throw new Error('useCommandMenu was used outside of CommandMenuProvider')
}
return context
}
export interface CommandMenuProviderProps {
site: 'studio' | 'docs' | 'website'
projectRef?: string
/**
* Project's API keys, for easy access through CMDK
*/
apiKeys?: { anon?: string; service?: string }
apiUrl?: string
/**
* Opt in flag to use additional metadata in AI prompts
*/
isOptedInToAI?: boolean
/**
* Any additional metadata that CMDK component can use in its AI prompts
*/
metadata?: { definitions?: string; flags?: { [key: string]: string } }
/**
* Call back when save SQL snippet button is selected
*/
saveGeneratedSQL?: (answer: string, title: string) => Promise
}
const CommandMenuProvider = ({
children,
site,
projectRef,
apiKeys,
apiUrl,
metadata,
isOptedInToAI = false,
saveGeneratedSQL,
}: PropsWithChildren) => {
const [isOpen, setIsOpen] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [search, setSearch] = useState('')
const [pages, setPages] = useState([])
const inputRef = useRef>(null)
const currentPage = pages[pages.length - 1]
const project = projectRef !== undefined ? { ref: projectRef, apiKeys, apiUrl } : undefined
useKeyboardEvents({ setIsOpen, currentPage, setSearch, setPages })
return (
{children}
)
}
function useKeyboardEvents({
setIsOpen,
currentPage,
setSearch,
setPages,
}: {
setIsOpen: (isOpen: boolean) => void
setSearch: React.Dispatch>
setPages: React.Dispatch>
currentPage?: string
}) {
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'Escape':
event.preventDefault()
// if on homepage, close the command palette
if (!currentPage) setIsOpen(false)
setSearch('')
// if NOT on homepage, return to last page
setPages((pages) => pages.slice(0, -1))
return
case 'k':
case '/':
if (event.metaKey || event.ctrlKey) {
// Some browsers (ie. firefox) will focus the address bar by default
event.preventDefault()
setIsOpen(true)
}
return
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [setIsOpen, currentPage])
}
export default CommandMenuProvider