Move to TanStack Start from Next.js (#184)

This commit is contained in:
Aslam
2024-10-07 16:44:17 +07:00
committed by GitHub
parent 3a89a1c07f
commit 950ebc3dad
514 changed files with 20021 additions and 15508 deletions

View File

@@ -0,0 +1,142 @@
import { icons } from "lucide-react"
import { LaAccount } from "@/lib/schema"
import { HTMLLikeElement } from "@/lib/utils"
import { useCommandActions } from "~/hooks/use-command-actions"
export type CommandAction = string | (() => void)
export interface CommandItemType {
id?: string
icon?: keyof typeof icons
value: string
label: HTMLLikeElement | string
action: CommandAction
payload?: any
shortcut?: string
}
export type CommandGroupType = Array<{
heading?: string
items: CommandItemType[]
}>
const createNavigationItem = (
icon: keyof typeof icons,
value: string,
path: string,
actions: ReturnType<typeof useCommandActions>,
): CommandItemType => ({
icon,
value: `Go to ${value}`,
label: {
tag: "span",
children: [
"Go to ",
{
tag: "span",
attributes: { className: "font-semibold" },
children: [value],
},
],
},
action: () => actions.navigateTo(path),
})
export const createCommandGroups = (
actions: ReturnType<typeof useCommandActions>,
me: LaAccount,
): Record<string, CommandGroupType> => ({
home: [
{
heading: "General",
items: [
{
icon: "SunMoon",
value: "Change Theme...",
label: "Change Theme...",
action: "CHANGE_PAGE",
payload: "changeTheme",
},
{
icon: "Copy",
value: "Copy Current URL",
label: "Copy Current URL",
action: actions.copyCurrentURL,
},
],
},
{
heading: "Personal Links",
items: [
{
icon: "TextSearch",
value: "Search Links...",
label: "Search Links...",
action: "CHANGE_PAGE",
payload: "searchLinks",
},
{
icon: "Plus",
value: "Create New Link...",
label: "Create New Link...",
action: () => actions.navigateTo("/links?create=true"),
},
],
},
{
heading: "Personal Pages",
items: [
{
icon: "FileSearch",
value: "Search Pages...",
label: "Search Pages...",
action: "CHANGE_PAGE",
payload: "searchPages",
},
{
icon: "Plus",
value: "Create New Page...",
label: "Create New Page...",
action: () => actions.createNewPage(me),
},
],
},
{
heading: "Navigation",
items: [
createNavigationItem("ArrowRight", "Links", "/links", actions),
createNavigationItem("ArrowRight", "Pages", "/pages", actions),
createNavigationItem("ArrowRight", "Search", "/search", actions),
createNavigationItem("ArrowRight", "Profile", "/profile", actions),
createNavigationItem("ArrowRight", "Settings", "/settings", actions),
],
},
],
searchLinks: [],
searchPages: [],
topics: [],
changeTheme: [
{
items: [
{
icon: "Moon",
value: "Change Theme to Dark",
label: "Change Theme to Dark",
action: () => actions.changeTheme("dark"),
},
{
icon: "Sun",
value: "Change Theme to Light",
label: "Change Theme to Light",
action: () => actions.changeTheme("light"),
},
{
icon: "Monitor",
value: "Change Theme to System",
label: "Change Theme to System",
action: () => actions.changeTheme("system"),
},
],
},
],
})

View File

@@ -0,0 +1,73 @@
import * as React from "react"
import { Command } from "cmdk"
import { CommandSeparator, CommandShortcut } from "@/components/ui/command"
import { LaIcon } from "@/components/custom/la-icon"
import { CommandItemType, CommandAction } from "./command-data"
import { HTMLLikeElement, renderHTMLLikeElement } from "@/lib/utils"
export interface CommandItemProps extends Omit<CommandItemType, "action"> {
action: CommandAction
handleAction: (action: CommandAction, payload?: any) => void
}
const HTMLLikeRenderer: React.FC<{ content: HTMLLikeElement | string }> =
React.memo(({ content }) => {
return (
<span className="line-clamp-1">{renderHTMLLikeElement(content)}</span>
)
})
HTMLLikeRenderer.displayName = "HTMLLikeRenderer"
export const CommandItem: React.FC<CommandItemProps> = React.memo(
({ icon, label, action, payload, shortcut, handleAction, ...item }) => (
<Command.Item
value={`${item.id}-${item.value}`}
onSelect={() => handleAction(action, payload)}
>
{icon && <LaIcon name={icon} />}
<HTMLLikeRenderer content={label} />
{shortcut && <CommandShortcut>{shortcut}</CommandShortcut>}
</Command.Item>
),
)
CommandItem.displayName = "CommandItem"
export interface CommandGroupProps {
heading?: string
items: CommandItemType[]
handleAction: (action: CommandAction, payload?: any) => void
isLastGroup: boolean
}
export const CommandGroup: React.FC<CommandGroupProps> = React.memo(
({ heading, items, handleAction, isLastGroup }) => {
return (
<>
{heading ? (
<Command.Group heading={heading}>
{items.map((item, index) => (
<CommandItem
key={`${heading}-${item.label}-${index}`}
{...item}
handleAction={handleAction}
/>
))}
</Command.Group>
) : (
items.map((item, index) => (
<CommandItem
key={`item-${item.label}-${index}`}
{...item}
handleAction={handleAction}
/>
))
)}
{!isLastGroup && <CommandSeparator className="my-1.5" />}
</>
)
},
)
CommandGroup.displayName = "CommandGroup"

View File

@@ -0,0 +1,214 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Command } from "cmdk"
import {
Dialog,
DialogPortal,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog"
import { CommandGroup } from "./command-group"
import { CommandAction, createCommandGroups } from "./command-data"
import { useAccount, useAccountOrGuest } from "@/lib/providers/jazz-provider"
import { useAtom } from "jotai"
import { useCommandActions } from "~/hooks/use-command-actions"
import {
filterItems,
getTopics,
getPersonalLinks,
getPersonalPages,
handleAction,
} from "./utils"
import { searchSafeRegExp } from "~/lib/utils"
import { commandPaletteOpenAtom } from "~/store/any-store"
export function CommandPalette() {
const { me } = useAccountOrGuest()
if (me._type === "Anonymous") return null
return <RealCommandPalette />
}
export function RealCommandPalette() {
const { me } = useAccount({ root: { personalLinks: [], personalPages: [] } })
const dialogRef = React.useRef<HTMLDivElement | null>(null)
const [inputValue, setInputValue] = React.useState("")
const [activePage, setActivePage] = React.useState("home")
const [open, setOpen] = useAtom(commandPaletteOpenAtom)
const actions = useCommandActions()
const commandGroups = React.useMemo(
() => me && createCommandGroups(actions, me),
[actions, me],
)
const bounce = React.useCallback(() => {
if (dialogRef.current) {
dialogRef.current.style.transform = "scale(0.99) translateX(-50%)"
setTimeout(() => {
if (dialogRef.current) {
dialogRef.current.style.transform = ""
}
}, 100)
}
}, [])
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter") {
bounce()
}
if (activePage !== "home" && !inputValue && e.key === "Backspace") {
e.preventDefault()
setActivePage("home")
setInputValue("")
bounce()
}
},
[activePage, inputValue, bounce],
)
const topics = React.useMemo(() => getTopics(actions), [actions])
const personalLinks = React.useMemo(
() => getPersonalLinks(me?.root.personalLinks || [], actions),
[me?.root.personalLinks, actions],
)
const personalPages = React.useMemo(
() => getPersonalPages(me?.root.personalPages || [], actions),
[me?.root.personalPages, actions],
)
const getFilteredCommands = React.useCallback(() => {
if (!commandGroups) return []
const searchRegex = searchSafeRegExp(inputValue)
if (activePage === "home") {
if (!inputValue) {
return commandGroups.home
}
const allGroups = [
...Object.values(commandGroups).flat(),
personalLinks,
personalPages,
topics,
]
return allGroups
.map((group) => ({
heading: group.heading,
items: filterItems(group.items, searchRegex),
}))
.filter((group) => group.items.length > 0)
}
switch (activePage) {
case "searchLinks":
return [
...commandGroups.searchLinks,
{ items: filterItems(personalLinks.items, searchRegex) },
]
case "searchPages":
return [
...commandGroups.searchPages,
{ items: filterItems(personalPages.items, searchRegex) },
]
default: {
const pageCommands = commandGroups[activePage]
if (!inputValue) return pageCommands
return pageCommands
.map((group) => ({
heading: group.heading,
items: filterItems(group.items, searchRegex),
}))
.filter((group) => group.items.length > 0)
}
}
}, [
inputValue,
activePage,
commandGroups,
personalLinks,
personalPages,
topics,
])
const handleActionWrapper = React.useCallback(
(action: CommandAction, payload?: any) => {
handleAction(action, payload, {
setActivePage,
setInputValue,
bounce,
closeDialog: () => setOpen(false),
})
},
[bounce, setOpen],
)
const filteredCommands = React.useMemo(
() => getFilteredCommands(),
[getFilteredCommands],
)
const commandKey = React.useMemo(() => {
return filteredCommands
.map((group) => {
const itemsKey = group.items
.map((item) => `${item.label}-${item.value}`)
.join("|")
return `${group.heading}:${itemsKey}`
})
.join("__")
}, [filteredCommands])
if (!me) return null
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogPortal>
<DialogPrimitive.Overlay la-overlay="" cmdk-overlay="" />
<DialogPrimitive.Content
la-dialog=""
cmdk-dialog=""
className="la"
ref={dialogRef}
>
<DialogHeader className="sr-only">
<DialogTitle>Command Palette</DialogTitle>
<DialogDescription>
Search for commands and actions
</DialogDescription>
</DialogHeader>
<Command key={commandKey} onKeyDown={handleKeyDown}>
<div cmdk-input-wrapper="">
<Command.Input
autoFocus
placeholder="Type a command or search..."
value={inputValue}
onValueChange={setInputValue}
/>
</div>
<Command.List>
<Command.Empty>No results found.</Command.Empty>
{filteredCommands.map((group, index, array) => (
<CommandGroup
key={`${group.heading}-${index}`}
heading={group.heading}
items={group.items}
handleAction={handleActionWrapper}
isLastGroup={index === array.length - 1}
/>
))}
</Command.List>
</Command>
</DialogPrimitive.Content>
</DialogPortal>
</Dialog>
)
}

View File

@@ -0,0 +1,74 @@
import { GraphData } from "~/lib/constants"
import { CommandAction, CommandItemType } from "./command-data"
export const filterItems = (items: CommandItemType[], searchRegex: RegExp) =>
items.filter((item) => searchRegex.test(item.value)).slice(0, 10)
export const getTopics = (actions: { navigateTo: (path: string) => void }) => ({
heading: "Topics",
items: GraphData.map((topic) => ({
icon: "Circle" as const,
value: topic?.prettyName || "",
label: topic?.prettyName || "",
action: () => actions.navigateTo(`/${topic?.name}`),
})),
})
export const getPersonalLinks = (
personalLinks: any[],
actions: { openLinkInNewTab: (url: string) => void },
) => ({
heading: "Personal Links",
items: personalLinks.map((link) => ({
id: link?.id,
icon: "Link" as const,
value: link?.title || "Untitled",
label: link?.title || "Untitled",
action: () => actions.openLinkInNewTab(link?.url || "#"),
})),
})
export const getPersonalPages = (
personalPages: any[],
actions: { navigateTo: (path: string) => void },
) => ({
heading: "Personal Pages",
items: personalPages.map((page) => ({
id: page?.id,
icon: "FileText" as const,
value: page?.title || "Untitled",
label: page?.title || "Untitled",
action: () => actions.navigateTo(`/pages/${page?.id}`),
})),
})
export const handleAction = (
action: CommandAction,
payload: any,
callbacks: {
setActivePage: (page: string) => void
setInputValue: (value: string) => void
bounce: () => void
closeDialog: () => void
},
) => {
const { setActivePage, setInputValue, bounce, closeDialog } = callbacks
if (typeof action === "function") {
action()
closeDialog()
return
}
switch (action) {
case "CHANGE_PAGE":
if (payload) {
setActivePage(payload)
setInputValue("")
bounce()
}
break
default:
closeDialog()
}
}