diff --git a/web/components/custom/command-palette/command-data.ts b/web/components/custom/command-palette/command-data.ts index e6d7a3d1..80217afd 100644 --- a/web/components/custom/command-palette/command-data.ts +++ b/web/components/custom/command-palette/command-data.ts @@ -1,12 +1,14 @@ import { icons } from "lucide-react" import { useCommandActions } from "./hooks/use-command-actions" import { LaAccount } from "@/lib/schema" +import { HTMLLikeElement } from "@/lib/utils" export type CommandAction = string | (() => void) export type CommandItemType = { icon?: keyof typeof icons - label: string + value: string + label: HTMLLikeElement | string action: CommandAction payload?: any shortcut?: string @@ -25,9 +27,16 @@ export const createCommandGroups = ( { heading: "General", items: [ - { icon: "SunMoon", label: "Change Theme...", action: "CHANGE_PAGE", payload: "changeTheme" }, + { + 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 } @@ -36,20 +45,88 @@ export const createCommandGroups = ( { heading: "Personal Links", items: [ - { icon: "TextSearch", label: "Search Links...", action: "CHANGE_PAGE", payload: "searchLinks" }, - { icon: "Plus", label: "Create New Link...", action: () => actions.navigateTo("/") } + { + 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("/") + } ] }, { heading: "Personal Pages", items: [ - { icon: "FileSearch", label: "Search Pages...", action: "CHANGE_PAGE", payload: "searchPages" }, + { + 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: [ + { + icon: "ArrowRight", + value: "Go to Links", + label: { + tag: "span", + children: ["Go to ", { tag: "span", attributes: { className: "font-semibold" }, children: ["links"] }] + }, + action: () => actions.navigateTo("/links") + }, + { + icon: "ArrowRight", + value: "Go to Pages", + label: { + tag: "span", + children: ["Go to ", { tag: "span", attributes: { className: "font-semibold" }, children: ["pages"] }] + }, + action: () => actions.navigateTo("/pages") + }, + { + icon: "ArrowRight", + value: "Go to Search", + label: { + tag: "span", + children: ["Go to ", { tag: "span", attributes: { className: "font-semibold" }, children: ["search"] }] + }, + action: () => actions.navigateTo("/search") + }, + { + icon: "ArrowRight", + value: "Go to Profile", + label: { + tag: "span", + children: ["Go to ", { tag: "span", attributes: { className: "font-semibold" }, children: ["profile"] }] + }, + action: () => actions.navigateTo("/profile") + }, + { + icon: "ArrowRight", + value: "Go to Settings", + label: { + tag: "span", + children: ["Go to ", { tag: "span", attributes: { className: "font-semibold" }, children: ["settings"] }] + }, + action: () => actions.navigateTo("/settings") + } + ] } ], searchLinks: [], @@ -58,9 +135,24 @@ export const createCommandGroups = ( changeTheme: [ { items: [ - { icon: "Moon", label: "Change Theme to Dark", action: () => actions.changeTheme("dark") }, - { icon: "Sun", label: "Change Theme to Light", action: () => actions.changeTheme("light") }, - { icon: "Monitor", label: "Change Theme to System", action: () => actions.changeTheme("system") } + { + 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: "changeThemeToSystem", + label: "Change Theme to System", + action: () => actions.changeTheme("system") + } ] } ] diff --git a/web/components/custom/command-palette/command-items.tsx b/web/components/custom/command-palette/command-items.tsx index 0404a01e..64b09602 100644 --- a/web/components/custom/command-palette/command-items.tsx +++ b/web/components/custom/command-palette/command-items.tsx @@ -2,16 +2,21 @@ import { Command } from "cmdk" import { CommandSeparator, CommandShortcut } from "@/components/ui/command" import { LaIcon } from "../la-icon" import { CommandItemType, CommandAction } from "./command-data" +import { HTMLLikeElement, renderHTMLLikeElement } from "@/lib/utils" export interface CommandItemProps extends Omit { action: CommandAction handleAction: (action: CommandAction, payload?: any) => void } +const HTMLLikeRenderer: React.FC<{ content: HTMLLikeElement | string }> = ({ content }) => { + return <>{renderHTMLLikeElement(content)} +} + export const CommandItem: React.FC = ({ icon, label, action, payload, shortcut, handleAction }) => ( handleAction(action, payload)}> {icon && } - {label} + {shortcut && {shortcut}} ) diff --git a/web/components/custom/command-palette/command-palette.tsx b/web/components/custom/command-palette/command-palette.tsx index a6b5386a..4986d557 100644 --- a/web/components/custom/command-palette/command-palette.tsx +++ b/web/components/custom/command-palette/command-palette.tsx @@ -14,7 +14,7 @@ import { useCommandActions } from "./hooks/use-command-actions" let graph_data_promise = import("@/components/routes/public/graph-data.json").then(a => a.default) const filterItems = (items: CommandItemType[], searchRegex: RegExp) => - items.filter(item => searchRegex.test(item.label)).slice(0, 6) + items.filter(item => searchRegex.test(item.value)).slice(0, 6) export function CommandPalette() { const { me } = useAccount({ root: { personalLinks: [], personalPages: [] } }) @@ -81,6 +81,7 @@ export function CommandPalette() { heading: "Topics", items: raw_graph_data.map(topic => ({ icon: "Circle" as const, + value: topic?.prettyName || "", label: topic?.prettyName || "", action: () => actions.navigateTo(`/${topic?.name}`) })) @@ -94,6 +95,7 @@ export function CommandPalette() { items: me?.root.personalLinks?.map(link => ({ icon: "Link" as const, + value: link?.title || "Untitled", label: link?.title || "Untitled", action: () => actions.openLinkInNewTab(link?.url || "#") })) || [] @@ -107,6 +109,7 @@ export function CommandPalette() { items: me?.root.personalPages?.map(page => ({ icon: "FileText" as const, + value: page?.title || "Untitled", label: page?.title || "Untitled", action: () => actions.navigateTo(`/pages/${page?.id}`) })) || [] diff --git a/web/jest.config.ts b/web/jest.config.ts index 694d51c8..2e971f9f 100644 --- a/web/jest.config.ts +++ b/web/jest.config.ts @@ -1,11 +1,14 @@ import type { Config } from "jest" import nextJest from "next/jest.js" +import dotenv from "dotenv" const createJestConfig = nextJest({ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment dir: "./" }) +dotenv.config({ path: ".env.local" }) + // Add any custom config to be passed to Jest const config: Config = { coverageProvider: "v8", @@ -13,7 +16,8 @@ const config: Config = { // Add more setup options before each test is run // setupFilesAfterEnv: ['/jest.setup.ts'], // Automatically clear mock calls, instances, contexts and results before every test - clearMocks: true + clearMocks: true, + moduleDirectories: ["node_modules", __dirname] } // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/web/lib/utils/htmlLikeElementUtil.test.tsx b/web/lib/utils/htmlLikeElementUtil.test.tsx new file mode 100644 index 00000000..3da4d3ff --- /dev/null +++ b/web/lib/utils/htmlLikeElementUtil.test.tsx @@ -0,0 +1,45 @@ +import React from "react" +import { render } from "@testing-library/react" +import { HTMLLikeElement, renderHTMLLikeElement } from "./htmlLikeElementUtil" + +const HTMLLikeRenderer: React.FC<{ content: HTMLLikeElement | string }> = ({ content }) => { + return <>{renderHTMLLikeElement(content)} +} + +describe("HTML-like Element Utility", () => { + test("HTMLLikeRenderer renders simple string content", () => { + const { getByText } = render() + expect(getByText("Hello, World!")).toBeTruthy() + }) + + test("HTMLLikeRenderer renders HTML-like structure", () => { + const content: HTMLLikeElement = { + tag: "div", + attributes: { className: "test-class" }, + children: ["Hello, ", { tag: "strong", children: ["World"] }, "!"] + } + const { container, getByText } = render() + expect(container.firstChild).toHaveProperty("className", "test-class") + const strongElement = getByText("World") + expect(strongElement.tagName.toLowerCase()).toBe("strong") + }) + + test("HTMLLikeRenderer handles multiple attributes", () => { + const content: HTMLLikeElement = { + tag: "div", + attributes: { + className: "test-class", + id: "test-id", + "data-testid": "custom-element", + style: { color: "red", fontSize: "16px" } + }, + children: ["Test Content"] + } + const { getByTestId } = render() + const element = getByTestId("custom-element") + expect(element.className).toBe("test-class") + expect(element.id).toBe("test-id") + expect(element.style.color).toBe("red") + expect(element.style.fontSize).toBe("16px") + }) +}) diff --git a/web/lib/utils/htmlLikeElementUtil.ts b/web/lib/utils/htmlLikeElementUtil.ts new file mode 100644 index 00000000..6246c5c0 --- /dev/null +++ b/web/lib/utils/htmlLikeElementUtil.ts @@ -0,0 +1,21 @@ +import React from "react" + +export type HTMLAttributes = React.HTMLAttributes & { + [key: string]: any +} + +export type HTMLLikeElement = { + tag: keyof JSX.IntrinsicElements + attributes?: HTMLAttributes + children?: (HTMLLikeElement | string)[] +} + +export const renderHTMLLikeElement = (element: HTMLLikeElement | string): React.ReactNode => { + if (typeof element === "string") { + return element + } + + const { tag, attributes = {}, children = [] } = element + + return React.createElement(tag, attributes, ...children.map(child => renderHTMLLikeElement(child))) +} diff --git a/web/lib/utils/index.ts b/web/lib/utils/index.ts index 92a6b8e9..7e4dfbe3 100644 --- a/web/lib/utils/index.ts +++ b/web/lib/utils/index.ts @@ -37,3 +37,4 @@ export function shuffleArray(array: T[]): T[] { export * from "./urls" export * from "./slug" export * from "./keyboard" +export * from "./htmlLikeElementUtil" diff --git a/web/package.json b/web/package.json index f5775f62..de3bc5d0 100644 --- a/web/package.json +++ b/web/package.json @@ -70,8 +70,8 @@ "date-fns": "^3.6.0", "framer-motion": "^11.5.4", "geist": "^1.3.1", - "jazz-react": "0.7.35-guest-auth.5", "jazz-browser-auth-clerk": "0.7.35-guest-auth.5", + "jazz-react": "0.7.35-guest-auth.5", "jazz-react-auth-clerk": "0.7.35-guest-auth.5", "jazz-tools": "0.7.35-guest-auth.5", "jotai": "^2.9.3", @@ -91,7 +91,6 @@ "streaming-markdown": "^0.0.14", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", - "ts-node": "^10.9.2", "zod": "^3.23.8", "zsa": "^0.6.0" }, @@ -102,6 +101,7 @@ "@types/node": "^22.5.4", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", + "dotenv": "^16.4.5", "eslint": "^8.57.0", "eslint-config-next": "14.2.5", "jest": "^29.7.0", @@ -110,6 +110,7 @@ "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.10", "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "typescript": "^5.5.4" } }