diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 9a67c6862..c0df17f34 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -19,6 +19,7 @@ "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0", + "@homarr/gridstack": "^1.0.0", "@homarr/notifications": "workspace:^0.1.0", "@homarr/spotlight": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", @@ -29,9 +30,9 @@ "@mantine/modals": "^7.5.3", "@mantine/tiptap": "^7.5.3", "@t3-oss/env-nextjs": "^0.9.2", - "@tanstack/react-query": "^5.20.5", - "@tanstack/react-query-devtools": "^5.21.0", - "@tanstack/react-query-next-experimental": "5.20.5", + "@tanstack/react-query": "^5.21.2", + "@tanstack/react-query-devtools": "^5.21.3", + "@tanstack/react-query-next-experimental": "5.21.2", "@tiptap/extension-link": "^2.2.3", "@tiptap/react": "^2.2.3", "@tiptap/starter-kit": "^2.2.3", @@ -40,22 +41,22 @@ "@trpc/react-query": "next", "@trpc/server": "next", "dayjs": "^1.11.10", - "@homarr/gridstack": "^1.0.0", "jotai": "^2.6.4", "mantine-modal-manager": "^7.5.2", - "next": "^14.1.0", + "next": "^14.1.1-canary.58", "postcss-preset-mantine": "^1.13.0", "react": "18.2.0", "react-dom": "18.2.0", - "sass": "^1.71.0", - "superjson": "2.2.1" + "superjson": "2.2.1", + "use-deep-compare-effect": "^1.8.1", + "sass": "^1.71.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/node": "^20.11.19", - "@types/react": "^18.2.55", + "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "dotenv-cli": "^7.3.0", "eslint": "^8.56.0", diff --git a/apps/nextjs/src/app/[locale]/(main)/integrations/edit/[id]/page.tsx b/apps/nextjs/src/app/[locale]/(main)/integrations/edit/[id]/page.tsx index c903ff163..3a6063fcc 100644 --- a/apps/nextjs/src/app/[locale]/(main)/integrations/edit/[id]/page.tsx +++ b/apps/nextjs/src/app/[locale]/(main)/integrations/edit/[id]/page.tsx @@ -14,7 +14,7 @@ export default async function EditIntegrationPage({ params, }: EditIntegrationPageProps) { const t = await getScopedI18n("integration.page.edit"); - const integration = await api.integration.byId.query({ id: params.id }); + const integration = await api.integration.byId({ id: params.id }); return ( diff --git a/apps/nextjs/src/app/[locale]/(main)/integrations/page.tsx b/apps/nextjs/src/app/[locale]/(main)/integrations/page.tsx index 5c7f706c9..2b5549d94 100644 --- a/apps/nextjs/src/app/[locale]/(main)/integrations/page.tsx +++ b/apps/nextjs/src/app/[locale]/(main)/integrations/page.tsx @@ -47,7 +47,7 @@ interface IntegrationsPageProps { export default async function IntegrationsPage({ searchParams, }: IntegrationsPageProps) { - const integrations = await api.integration.all.query(); + const integrations = await api.integration.all(); const t = await getScopedI18n("integration"); return ( diff --git a/apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx b/apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx new file mode 100644 index 000000000..908c05046 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx @@ -0,0 +1,8 @@ +"use client"; + +import type { PropsWithChildren } from "react"; +import { Provider } from "jotai"; + +export const JotaiProvider = ({ children }: PropsWithChildren) => { + return {children}; +}; diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx index 187c5c9f1..dcf7d63a1 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx @@ -1,5 +1,6 @@ "use client"; +import type { PropsWithChildren } from "react"; import { useState } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; @@ -9,19 +10,7 @@ import superjson from "superjson"; import { clientApi } from "@homarr/api/client"; -import { env } from "~/env.mjs"; - -const getBaseUrl = () => { - if (typeof window !== "undefined") return ""; // browser should use relative url - if (env.VERCEL_URL) return env.VERCEL_URL; // SSR should use vercel url - - return `http://localhost:${env.PORT}`; // dev SSR should use localhost -}; - -export function TRPCReactProvider(props: { - children: React.ReactNode; - headers?: Headers; -}) { +export function TRPCReactProvider(props: PropsWithChildren) { const [queryClient] = useState( () => new QueryClient({ @@ -35,7 +24,6 @@ export function TRPCReactProvider(props: { const [trpcClient] = useState(() => clientApi.createClient({ - transformer: superjson, links: [ loggerLink({ enabled: (opts) => @@ -43,11 +31,12 @@ export function TRPCReactProvider(props: { (opts.direction === "down" && opts.result instanceof Error), }), unstable_httpBatchStreamLink({ - url: `${getBaseUrl()}/api/trpc`, + transformer: superjson, + url: getBaseUrl() + "/api/trpc", headers() { - const headers = new Map(props.headers); + const headers = new Headers(); headers.set("x-trpc-source", "nextjs-react"); - return Object.fromEntries(headers); + return headers; }, }), ], @@ -65,3 +54,9 @@ export function TRPCReactProvider(props: { ); } + +function getBaseUrl() { + if (typeof window !== "undefined") return window.location.origin; + if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; + return `http://localhost:${process.env.PORT ?? 3000}`; +} diff --git a/apps/nextjs/src/app/[locale]/boards/(default)/_definition.ts b/apps/nextjs/src/app/[locale]/boards/(default)/_definition.ts index cf95e7c7c..af962bc05 100644 --- a/apps/nextjs/src/app/[locale]/boards/(default)/_definition.ts +++ b/apps/nextjs/src/app/[locale]/boards/(default)/_definition.ts @@ -3,6 +3,6 @@ import { createBoardPage } from "../_creator"; export default createBoardPage<{ locale: string }>({ async getInitialBoard() { - return await api.board.default.query(); + return await api.board.default(); }, }); diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/_definition.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/_definition.tsx index 5436994e2..56cdebce8 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/_definition.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/_definition.tsx @@ -3,6 +3,6 @@ import { createBoardPage } from "../_creator"; export default createBoardPage<{ locale: string; name: string }>({ async getInitialBoard({ name }) { - return await api.board.byName.query({ name }); + return await api.board.byName({ name }); }, }); diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx index fcfe89938..1b156881b 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx @@ -28,7 +28,7 @@ interface Props { } export default async function BoardSettingsPage({ params }: Props) { - const board = await api.board.byName.query({ name: params.name }); + const board = await api.board.byName({ name: params.name }); const t = await getScopedI18n("board.setting"); return ( diff --git a/apps/nextjs/src/app/[locale]/compose.tsx b/apps/nextjs/src/app/[locale]/compose.tsx new file mode 100644 index 000000000..cddd9dcda --- /dev/null +++ b/apps/nextjs/src/app/[locale]/compose.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +type PropsWithChildren = Required; + +export const composeWrappers = ( + wrappers: React.FunctionComponent[], +): React.FunctionComponent => { + return wrappers + .reverse() + .reduce((Acc, Current): React.FunctionComponent => { + // eslint-disable-next-line react/display-name + return (props) => ( + + + + ); + }); +}; diff --git a/apps/nextjs/src/app/[locale]/init/user/_init-user-form.tsx b/apps/nextjs/src/app/[locale]/init/user/_init-user-form.tsx index af52171e4..950ec10c8 100644 --- a/apps/nextjs/src/app/[locale]/init/user/_init-user-form.tsx +++ b/apps/nextjs/src/app/[locale]/init/user/_init-user-form.tsx @@ -30,7 +30,6 @@ export const InitUserForm = () => { }); const handleSubmit = async (values: FormType) => { - console.log(values); await mutateAsync(values, { onSuccess: () => { showSuccessNotification({ diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index d74b5e917..c24ad14c2 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -1,11 +1,9 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import { Inter } from "next/font/google"; -import "@homarr/ui/styles.css"; import "@homarr/notifications/styles.css"; import "@homarr/spotlight/styles.css"; - -import { headers } from "next/headers"; +import "@homarr/ui/styles.css"; import { Notifications } from "@homarr/notifications"; import { @@ -14,25 +12,39 @@ import { uiConfiguration, } from "@homarr/ui"; +import { JotaiProvider } from "./_client-providers/jotai"; import { ModalsProvider } from "./_client-providers/modals"; import { NextInternationalProvider } from "./_client-providers/next-international"; import { TRPCReactProvider } from "./_client-providers/trpc"; +import { composeWrappers } from "./compose"; const fontSans = Inter({ subsets: ["latin"], variable: "--font-sans", }); -/** - * Since we're passing `headers()` to the `TRPCReactProvider` we need to - * make the entire app dynamic. You can move the `TRPCReactProvider` further - * down the tree (e.g. /dashboard and onwards) to make part of the app statically rendered. - */ -export const dynamic = "force-dynamic"; - export const metadata: Metadata = { + metadataBase: new URL("http://localhost:3000"), title: "Create T3 Turbo", description: "Simple monorepo with shared backend for web & mobile apps", + openGraph: { + title: "Create T3 Turbo", + description: "Simple monorepo with shared backend for web & mobile apps", + url: "https://create-t3-turbo.vercel.app", + siteName: "Create T3 Turbo", + }, + twitter: { + card: "summary_large_image", + site: "@jullerino", + creator: "@jullerino", + }, +}; + +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: light)", color: "white" }, + { media: "(prefers-color-scheme: dark)", color: "black" }, + ], }; export default function Layout(props: { @@ -41,25 +53,32 @@ export default function Layout(props: { }) { const colorScheme = "dark"; + const StackedProvider = composeWrappers([ + (innerProps) => , + (innerProps) => , + (innerProps) => ( + + ), + (innerProps) => ( + + ), + (innerProps) => , + ]); + return ( - + - - - - - - {props.children} - - - - + + + {props.children} + ); diff --git a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx index a5baaa881..2437439af 100644 --- a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx @@ -10,7 +10,7 @@ import { DeleteBoardButton } from "./_components/delete-board-button"; export default async function ManageBoardsPage() { const t = await getScopedI18n("management.page.board"); - const boards = await api.board.getAll.query(); + const boards = await api.board.getAll(); return ( <> diff --git a/apps/nextjs/src/app/api/trpc/[trpc]/route.ts b/apps/nextjs/src/app/api/trpc/[trpc]/route.ts index ed3dcfaaa..ef65a1ee6 100644 --- a/apps/nextjs/src/app/api/trpc/[trpc]/route.ts +++ b/apps/nextjs/src/app/api/trpc/[trpc]/route.ts @@ -28,7 +28,7 @@ const handler = auth(async (req) => { router: appRouter, req, createContext: () => - createTRPCContext({ auth: req.auth, headers: req.headers }), + createTRPCContext({ session: req.auth, headers: req.headers }), onError({ error, path }) { console.error(`>>> tRPC Error on '${path}'`, error); }, diff --git a/apps/nextjs/src/components/board/sections/content.tsx b/apps/nextjs/src/components/board/sections/content.tsx index bc6d21af7..26d9ffb99 100644 --- a/apps/nextjs/src/components/board/sections/content.tsx +++ b/apps/nextjs/src/components/board/sections/content.tsx @@ -34,26 +34,28 @@ interface Props { export const SectionContent = ({ items, refs }: Props) => { return ( <> - {items.map((item) => ( -
} - > - - - -
- ))} + {items.map((item) => { + return ( +
} + > + + + +
+ ); + })} ); }; diff --git a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts index 6dcad138d..909b34297 100644 --- a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts +++ b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts @@ -107,7 +107,7 @@ export const useGridstack = ({ // Add listener for moving items in config from one wrapper to another currentGrid?.on("added", (_, nodes) => { - nodes.forEach((node) => onAdd(node)); + nodes.forEach(onAdd); }); return () => { @@ -192,8 +192,6 @@ const useCssVariableConfiguration = ({ const widgetWidth = mainRef.current.clientWidth / sectionColumnCount; // widget width is used to define sizes of gridstack items within global.scss root?.style.setProperty("--gridstack-widget-width", widgetWidth.toString()); - console.log("widgetWidth", widgetWidth); - console.log(gridRef.current); gridRef.current?.cellHeight(widgetWidth); // gridRef.current is required otherwise the cellheight is run on production as undefined // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/nextjs/src/components/layout/header.tsx b/apps/nextjs/src/components/layout/header.tsx index c9c06744a..65b9e76a5 100644 --- a/apps/nextjs/src/components/layout/header.tsx +++ b/apps/nextjs/src/components/layout/header.tsx @@ -1,11 +1,11 @@ import type { ReactNode } from "react"; import Link from "next/link"; +import { Spotlight } from "@homarr/spotlight"; import { AppShellHeader, Group, UnstyledButton } from "@homarr/ui"; import { ClientBurger } from "./header/burger"; import { DesktopSearchInput, MobileSearchButton } from "./header/search"; -import { ClientSpotlight } from "./header/spotlight"; import { UserButton } from "./header/user"; import { HomarrLogoWithTitle } from "./logo/homarr-logo"; @@ -38,7 +38,7 @@ export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => { - + ); }; diff --git a/apps/nextjs/src/components/layout/header/search.tsx b/apps/nextjs/src/components/layout/header/search.tsx index dcccd3360..9cdf5b56e 100644 --- a/apps/nextjs/src/components/layout/header/search.tsx +++ b/apps/nextjs/src/components/layout/header/search.tsx @@ -1,6 +1,6 @@ "use client"; -import { spotlight } from "@homarr/spotlight"; +import { openSpotlight } from "@homarr/spotlight"; import { useScopedI18n } from "@homarr/translation/client"; import { IconSearch, TextInput, UnstyledButton } from "@homarr/ui"; @@ -17,7 +17,7 @@ export const DesktopSearchInput = () => { w={400} size="sm" leftSection={} - onClick={spotlight.open} + onClick={openSpotlight} > {t("placeholder")} @@ -26,7 +26,7 @@ export const DesktopSearchInput = () => { export const MobileSearchButton = () => { return ( - + ); diff --git a/apps/nextjs/src/components/layout/header/spotlight.tsx b/apps/nextjs/src/components/layout/header/spotlight.tsx deleted file mode 100644 index 61d2f70b4..000000000 --- a/apps/nextjs/src/components/layout/header/spotlight.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import { Spotlight } from "@homarr/spotlight"; -import { useScopedI18n } from "@homarr/translation/client"; -import { IconSearch } from "@homarr/ui"; - -export const ClientSpotlight = () => { - const t = useScopedI18n("common.search"); - - return ( - , - placeholder: `${t("placeholder")}`, - }} - yOffset={12} - /> - ); -}; diff --git a/apps/nextjs/src/trpc/server.ts b/apps/nextjs/src/trpc/server.ts index ac38dce73..e014de061 100644 --- a/apps/nextjs/src/trpc/server.ts +++ b/apps/nextjs/src/trpc/server.ts @@ -1,12 +1,7 @@ import { cache } from "react"; import { headers } from "next/headers"; -import { createTRPCClient, loggerLink, TRPCClientError } from "@trpc/client"; -import { callProcedure } from "@trpc/server"; -import { observable } from "@trpc/server/observable"; -import type { TRPCErrorResponse } from "@trpc/server/rpc"; -import SuperJSON from "superjson"; -import { appRouter, createTRPCContext } from "@homarr/api"; +import { createCaller, createTRPCContext } from "@homarr/api"; import { auth } from "@homarr/auth"; /** @@ -18,44 +13,9 @@ const createContext = cache(async () => { heads.set("x-trpc-source", "rsc"); return createTRPCContext({ - auth: await auth(), + session: await auth(), headers: heads, }); }); -export const api = createTRPCClient({ - transformer: SuperJSON, - links: [ - loggerLink({ - enabled: (op) => - process.env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), - }), - /** - * Custom RSC link that invokes procedures directly in the server component Don't be too afraid - * about the complexity here, it's just wrapping `callProcedure` with an observable to make it a - * valid ending link for tRPC. - */ - () => - ({ op }) => - observable((observer) => { - createContext() - .then((ctx) => { - return callProcedure({ - procedures: appRouter._def.procedures, - path: op.path, - getRawInput: () => Promise.resolve(op.input), - ctx, - type: op.type, - }); - }) - .then((data) => { - observer.next({ result: { data } }); - observer.complete(); - }) - .catch((cause: TRPCErrorResponse) => { - observer.error(TRPCClientError.from(cause)); - }); - }), - ], -}); +export const api = createCaller(createContext); diff --git a/packages/api/index.ts b/packages/api/index.ts deleted file mode 100644 index 1903f05e0..000000000 --- a/packages/api/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; - -import type { AppRouter } from "./src/root"; - -export { appRouter, type AppRouter } from "./src/root"; -export { createTRPCContext } from "./src/trpc"; -/** - * Inference helpers for input types - * @example type HelloInput = RouterInputs['example']['hello'] - **/ -export type RouterInputs = inferRouterInputs; - -/** - * Inference helpers for output types - * @example type HelloOutput = RouterOutputs['example']['hello'] - **/ -export type RouterOutputs = inferRouterOutputs; diff --git a/packages/api/package.json b/packages/api/package.json index 1b84f6487..d464190ba 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -2,7 +2,7 @@ "name": "@homarr/api", "version": "0.1.0", "exports": { - ".": "./index.ts", + ".": "./src/index.ts", "./client": "./src/client.ts" }, "private": true, diff --git a/packages/api/src/client.ts b/packages/api/src/client.ts index a8f1f59aa..48f89771d 100644 --- a/packages/api/src/client.ts +++ b/packages/api/src/client.ts @@ -1,5 +1,5 @@ import { createTRPCReact } from "@trpc/react-query"; -import type { AppRouter } from ".."; +import type { AppRouter } from "."; export const clientApi = createTRPCReact(); diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 000000000..1cbe6fdd7 --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1,33 @@ +import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; + +import type { AppRouter } from "./root"; +import { appRouter } from "./root"; +import { createCallerFactory, createTRPCContext } from "./trpc"; + +/** + * Create a server-side caller for the tRPC API + * @example + * const trpc = createCaller(createContext); + * const res = await trpc.post.all(); + * ^? Post[] + */ +const createCaller = createCallerFactory(appRouter); + +/** + * Inference helpers for input types + * @example + * type PostByIdInput = RouterInputs['post']['byId'] + * ^? { id: number } + **/ +type RouterInputs = inferRouterInputs; + +/** + * Inference helpers for output types + * @example + * type AllPostsOutput = RouterOutputs['post']['all'] + * ^? Post[] + **/ +type RouterOutputs = inferRouterOutputs; + +export { createTRPCContext, appRouter, createCaller }; +export type { AppRouter, RouterInputs, RouterOutputs }; diff --git a/packages/api/src/router/test/board.spec.ts b/packages/api/src/router/test/board.spec.ts index 079fb4ccc..8d3e5afa5 100644 --- a/packages/api/src/router/test/board.spec.ts +++ b/packages/api/src/router/test/board.spec.ts @@ -13,7 +13,7 @@ import { } from "@homarr/db/schema/sqlite"; import { createDb } from "@homarr/db/test"; -import type { RouterOutputs } from "../../.."; +import type { RouterOutputs } from "../.."; import { boardRouter } from "../board"; // Mock the auth module to return an empty session diff --git a/packages/api/src/router/test/integration.spec.ts b/packages/api/src/router/test/integration.spec.ts index 11c14de18..cff84d82c 100644 --- a/packages/api/src/router/test/integration.spec.ts +++ b/packages/api/src/router/test/integration.spec.ts @@ -5,7 +5,7 @@ import { createId } from "@homarr/db"; import { integrations, integrationSecrets } from "@homarr/db/schema/sqlite"; import { createDb } from "@homarr/db/test"; -import type { RouterInputs } from "../../.."; +import type { RouterInputs } from "../.."; import { encryptSecret, integrationRouter } from "../integration"; import { expectToBeDefined } from "./board.spec"; diff --git a/packages/api/src/trpc.ts b/packages/api/src/trpc.ts index 4a13bb468..3ef11e351 100644 --- a/packages/api/src/trpc.ts +++ b/packages/api/src/trpc.ts @@ -17,49 +17,28 @@ import { ZodError } from "@homarr/validation"; /** * 1. CONTEXT * - * This section defines the "contexts" that are available in the backend API + * This section defines the "contexts" that are available in the backend API. * - * These allow you to access things like the database, the session, etc, when - * processing a request + * These allow you to access things when processing a request, like the database, the session, etc. * - */ -interface CreateContextOptions { - session: Session | null; -} - -/** - * This helper generates the "internals" for a tRPC context. If you need to use - * it, you can export it from here + * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each + * wrap this and provides the required context. * - * Examples of things you may need it for: - * - testing, so we dont have to mock Next.js' req/res - * - trpc's `createSSGHelpers` where we don't have req/res - * @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts - */ -const createInnerTRPCContext = (opts: CreateContextOptions) => { - return { - session: opts.session, - db, - }; -}; - -/** - * This is the actual context you'll use in your router. It will be used to - * process every request that goes through your tRPC endpoint - * @link https://trpc.io/docs/context + * @see https://trpc.io/docs/server/context */ export const createTRPCContext = async (opts: { - headers?: Headers; - auth: Session | null; + headers: Headers; + session: Session | null; }) => { - const session = opts.auth ?? (await auth()); - const source = opts.headers?.get("x-trpc-source") ?? "unknown"; + const session = opts.session ?? (await auth()); + const source = opts.headers.get("x-trpc-source") ?? "unknown"; console.log(">>> tRPC Request from", source, "by", session?.user); - return createInnerTRPCContext({ + return { session, - }); + db, + }; }; /** @@ -70,18 +49,21 @@ export const createTRPCContext = async (opts: { */ const t = initTRPC.context().create({ transformer: superjson, - errorFormatter({ shape, error }) { - return { - ...shape, - data: { - ...shape.data, - zodError: - error.cause instanceof ZodError ? error.cause.flatten() : null, - }, - }; - }, + errorFormatter: ({ shape, error }) => ({ + ...shape, + data: { + ...shape.data, + zodError: error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }), }); +/** + * Create a server-side caller + * @see https://trpc.io/docs/server/server-side-calls + */ +export const createCallerFactory = t.createCallerFactory; + /** * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) * diff --git a/packages/auth/env.mjs b/packages/auth/env.mjs index 9c260510b..3bcd30f32 100644 --- a/packages/auth/env.mjs +++ b/packages/auth/env.mjs @@ -7,18 +7,10 @@ export const env = createEnv({ process.env.NODE_ENV === "production" ? z.string().min(1) : z.string().min(1).optional(), - AUTH_URL: z.preprocess( - // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL - // Since NextAuth.js automatically uses the VERCEL_URL if present. - (str) => process.env.VERCEL_URL ?? str, - // VERCEL_URL doesn't include `https` so it cant be validated as a URL - process.env.VERCEL ? z.string() : z.string().url(), - ), }, client: {}, runtimeEnv: { AUTH_SECRET: process.env.AUTH_SECRET, - AUTH_URL: process.env.AUTH_URL, }, skipValidation: Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION), diff --git a/packages/auth/package.json b/packages/auth/package.json index 23bd81cea..3d65e6553 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -24,7 +24,7 @@ "@t3-oss/env-nextjs": "^0.9.2", "bcrypt": "^5.1.1", "cookies": "^0.9.1", - "next": "^14.1.0", + "next": "^14.1.1-canary.58", "next-auth": "5.0.0-beta.11", "react": "18.2.0", "react-dom": "18.2.0" diff --git a/packages/spotlight/ReadMe.md b/packages/spotlight/ReadMe.md new file mode 100644 index 000000000..5f6501798 --- /dev/null +++ b/packages/spotlight/ReadMe.md @@ -0,0 +1,146 @@ +# Spotlight + +Spotlight is the search functionality of Homarr. It can be opened by pressing `Ctrl + K` or `Cmd + K` on Mac. It is a quick way to search for anything in Homarr. + +## API + +### SpotlightActionData + +The [SpotlightActionData](./src/type.ts) is the data structure that is used to define the actions that are shown in the spotlight. + +#### Common properties + +| Name | Type | Description | +| ------------------------------ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| id | `string` | The id of the action. | +| title | `string \| (t: TranslationFunction) => string` | The title of the action. Either static or generated with translation function | +| description | `string \| (t: TranslationFunction) => string` | The description of the action. Either static or generated with translation function | +| icon | `string \| (props: TablerIconProps) => JSX.Element` | The icon of the action. Either a url to an image or a TablerIcon | +| group | `string` | The group of the action. By default the groups all, web and action exist. | +| ignoreSearchAndOnlyShowInGroup | `boolean` | If true, the action will only be shown in the group and not in the search results. | +| type | `'link' \| 'button'` | The type of the action. Either link or button | + +#### Properties for links + +| Name | Type | Description | +| ---- | -------- | ---------------------------------------------------------------------------------------------------------- | +| href | `string` | The url the link should navigate to. If %s is contained it will be replaced with the current search query. | + +#### Properties for buttons + +| Name | Type | Description | +| ------- | -------------------------- | ----------------------------------------------------------------------------------------- | +| onClick | `() => MaybePromise` | The function that should be called when the button is clicked. It can be async if needed. | + +### useRegisterSpotlightActions + +The [useRegisterSpotlightActions](./src/data-store.ts) hook is used to register actions to the spotlight. It takes an unique key and the array of [SpotlightActionData](#SpotlightActionData). + +#### Usage + +The following example shows how to use the `useRegisterSpotlightActions` hook to register an action to the spotlight. + +```tsx +"use client"; + +import { useRegisterSpotlightActions } from "@homarr/spotlight"; + +const MyComponent = () => { + useRegisterSpotlightActions("my-component", [ + { + id: "my-action", + title: "My Action", + description: "This is my action", + icon: "https://example.com/icon.png", + group: "web", + type: "link", + href: "https://example.com", + }, + ]); + + return
My Component
; +}; +``` + +##### Using translation function + +```tsx +"use client"; + +import { useRegisterSpotlightActions } from "@homarr/spotlight"; + +const MyComponent = () => { + useRegisterSpotlightActions("my-component", [ + { + id: "my-action", + title: (t) => t("some.path.to.translation.key"), + description: (t) => t("some.other.path.to.translation.key"), + icon: "https://example.com/icon.png", + group: "web", + type: "link", + href: "https://example.com", + }, + ]); + + return
Component implementation
; +}; +``` + +##### Using TablerIcon + +```tsx +"use client"; + +import { IconUserCog } from "tabler-react"; + +import { useRegisterSpotlightActions } from "@homarr/spotlight"; + +const UserMenu = () => { + useRegisterSpotlightActions("header-user-menu", [ + { + id: "user-preferences", + title: (t) => t("user.preferences.title"), + description: (t) => t("user.preferences.description"), + icon: IconUserCog, + group: "action", + type: "link", + href: "/user/preferences", + }, + ]); + + return
Component implementation
; +}; +``` + +##### Using dependency array + +```tsx +"use client"; + +import { IconUserCog } from "tabler-react"; + +import { useRegisterSpotlightActions } from "@homarr/spotlight"; + +const ColorSchemeButton = () => { + const { colorScheme, toggleColorScheme } = useColorScheme(); + + useRegisterSpotlightActions( + "toggle-color-scheme", + [ + { + id: "toggle-color-scheme", + title: (t) => t("common.colorScheme.toggle.title"), + description: (t) => + t(`common.colorScheme.toggle.${colorScheme}.description`), + icon: colorScheme === "light" ? IconSun : IconMoon, + group: "action", + type: "button", + onClick: toggleColorScheme, + }, + ], + [colorScheme], + ); + + return
Component implementation
; +}; +``` diff --git a/packages/spotlight/index.ts b/packages/spotlight/index.ts index 542a4b2c7..3bd16e178 100644 --- a/packages/spotlight/index.ts +++ b/packages/spotlight/index.ts @@ -1,2 +1 @@ export * from "./src"; -export { spotlight, Spotlight } from "@mantine/spotlight"; diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json index c1f64926f..5e34eb112 100644 --- a/packages/spotlight/package.json +++ b/packages/spotlight/package.json @@ -34,6 +34,8 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { - "@mantine/spotlight": "^7.5.3" + "@mantine/spotlight": "^7.5.3", + "@homarr/ui": "workspace:^0.1.0", + "@homarr/translation": "workspace:^0.1.0" } } diff --git a/packages/spotlight/src/chip-group.tsx b/packages/spotlight/src/chip-group.tsx new file mode 100644 index 000000000..73f86ffae --- /dev/null +++ b/packages/spotlight/src/chip-group.tsx @@ -0,0 +1,54 @@ +import { useScopedI18n } from "@homarr/translation/client"; +import { Chip } from "@homarr/ui"; + +import { + selectNextAction, + selectPreviousAction, + spotlightStore, + triggerSelectedAction, +} from "./spotlight-store"; +import type { SpotlightActionGroup } from "./type"; + +const disableArrowUpAndDown = (e: React.KeyboardEvent) => { + if (e.key === "ArrowDown") { + selectNextAction(spotlightStore); + e.preventDefault(); + } else if (e.key === "ArrowUp") { + selectPreviousAction(spotlightStore); + e.preventDefault(); + } else if (e.key === "Enter") { + triggerSelectedAction(spotlightStore); + } +}; + +const focusActiveByDefault = (e: React.FocusEvent) => { + const relatedTarget = e.relatedTarget; + + const isPreviousTargetRadio = + relatedTarget && "type" in relatedTarget && relatedTarget.type === "radio"; + if (isPreviousTargetRadio) return; + + const group = e.currentTarget.parentElement?.parentElement; + if (!group) return; + const label = group.querySelector("label[data-checked]"); + if (!label) return; + label.focus(); +}; + +interface Props { + group: SpotlightActionGroup; +} + +export const GroupChip = ({ group }: Props) => { + const t = useScopedI18n("common.search.group"); + return ( + + {t(group)} + + ); +}; diff --git a/packages/spotlight/src/component.module.css b/packages/spotlight/src/component.module.css new file mode 100644 index 000000000..1fa279615 --- /dev/null +++ b/packages/spotlight/src/component.module.css @@ -0,0 +1,7 @@ +.spotlightAction:hover { + background-color: alpha(var(--mantine-primary-color-filled), 0.1); +} + +.spotlightAction[data-selected="true"] { + background-color: alpha(var(--mantine-primary-color-filled), 0.3); +} diff --git a/packages/spotlight/src/component.tsx b/packages/spotlight/src/component.tsx new file mode 100644 index 000000000..04ab475b2 --- /dev/null +++ b/packages/spotlight/src/component.tsx @@ -0,0 +1,154 @@ +"use client"; + +import { useCallback, useState } from "react"; +import Link from "next/link"; +import { + Spotlight as MantineSpotlight, + SpotlightAction, +} from "@mantine/spotlight"; +import { useAtomValue } from "jotai"; + +import type { TranslationFunction } from "@homarr/translation"; +import { useI18n } from "@homarr/translation/client"; +import { + Center, + Chip, + Divider, + Flex, + Group, + IconSearch, + Text, +} from "@homarr/ui"; + +import { GroupChip } from "./chip-group"; +import classes from "./component.module.css"; +import { actionsAtomRead, groupsAtomRead } from "./data-store"; +import { setSelectedAction, spotlightStore } from "./spotlight-store"; +import type { SpotlightActionData } from "./type"; + +export const Spotlight = () => { + const [query, setQuery] = useState(""); + const [group, setGroup] = useState("all"); + const groups = useAtomValue(groupsAtomRead); + const actions = useAtomValue(actionsAtomRead); + const t = useI18n(); + + const preparedActions = actions.map((action) => prepareAction(action, t)); + const items = preparedActions + .filter( + (item) => + (item.ignoreSearchAndOnlyShowInGroup + ? item.group === group + : item.title.toLowerCase().includes(query.toLowerCase().trim())) && + (group === "all" || item.group === group), + ) + .map((item) => { + const renderRoot = + item.type === "link" + ? (props: Record) => ( + + ) + : undefined; + + return ( + + + {item.icon && ( +
+ {typeof item.icon !== "string" && } + {typeof item.icon === "string" && ( + {item.title} + )} +
+ )} + + + {item.title} + + {item.description && ( + + {item.description} + + )} + +
+
+ ); + }); + + const onGroupChange = useCallback( + (group: string) => { + setSelectedAction(-1, spotlightStore); + setGroup(group); + }, + [setGroup, setSelectedAction], + ); + + return ( + + } + /> + + + + + + {groups.map((group) => ( + + ))} + + + + + + {items.length > 0 ? ( + items + ) : ( + + {t("common.search.nothingFound")} + + )} + + + ); +}; + +const prepareHref = (href: string, query: string) => { + return href.replace("%s", query); +}; + +const translateIfNecessary = ( + value: string | ((t: TranslationFunction) => string), + t: TranslationFunction, +) => { + if (typeof value === "function") { + return value(t); + } + + return value; +}; + +const prepareAction = ( + action: SpotlightActionData, + t: TranslationFunction, +) => ({ + ...action, + title: translateIfNecessary(action.title, t), + description: translateIfNecessary(action.description, t), +}); diff --git a/packages/spotlight/src/data-store.ts b/packages/spotlight/src/data-store.ts new file mode 100644 index 000000000..31bcf1cba --- /dev/null +++ b/packages/spotlight/src/data-store.ts @@ -0,0 +1,72 @@ +import { useEffect } from "react"; +import { atom, useSetAtom } from "jotai"; +import useDeepCompareEffect from "use-deep-compare-effect"; + +import type { SpotlightActionData, SpotlightActionGroup } from "./type"; + +const defaultGroups = ["all", "web", "action"] as const; +const reversedDefaultGroups = [...defaultGroups].reverse(); +const actionsAtom = atom>({}); +export const actionsAtomRead = atom((get) => + Object.values(get(actionsAtom)).flatMap((item) => item), +); + +export const groupsAtomRead = atom((get) => + Array.from( + new Set( + get(actionsAtomRead) + .map((item) => item.group as SpotlightActionGroup) // Allow "all" group to be included in the list of groups + .concat(...defaultGroups), + ), + ) + .sort((groupA, groupB) => { + const groupAIndex = reversedDefaultGroups.indexOf(groupA); + const groupBIndex = reversedDefaultGroups.indexOf(groupB); + + // if both groups are not in the default groups, sort them by name (here reversed because we reverse the array afterwards) + if (groupAIndex === -1 && groupBIndex === -1) { + return groupB.localeCompare(groupA); + } + + return groupAIndex - groupBIndex; + }) + .reverse(), +); + +const registrations = new Map(); + +export const useRegisterSpotlightActions = ( + key: string, + actions: SpotlightActionData[], + dependencies: readonly unknown[] = [], +) => { + const setActions = useSetAtom(actionsAtom); + + // Use deep compare effect if there are dependencies for the actions, this supports deep compare of the action dependencies + const useSpecificEffect = + dependencies.length >= 1 ? useDeepCompareEffect : useEffect; + + useSpecificEffect(() => { + if (!registrations.has(key) || dependencies.length >= 1) { + setActions((prev) => ({ + ...prev, + [key]: actions, + })); + } + registrations.set(key, (registrations.get(key) ?? 0) + 1); + + return () => { + if (registrations.get(key) === 1) { + setActions((prev) => { + const { [key]: _, ...rest } = prev; + return rest; + }); + } + + registrations.set(key, (registrations.get(key) ?? 0) - 1); + if (registrations.get(key) === 0) { + registrations.delete(key); + } + }; + }, [key, dependencies.length >= 1 ? dependencies : undefined]); +}; diff --git a/packages/spotlight/src/index.ts b/packages/spotlight/src/index.ts index fcbc9a754..e7d5f2bb3 100644 --- a/packages/spotlight/src/index.ts +++ b/packages/spotlight/src/index.ts @@ -1 +1,8 @@ -export const name = "spotlight"; +"use client"; + +import { spotlightActions } from "./spotlight-store"; + +export { Spotlight } from "./component"; + +const openSpotlight = spotlightActions.open; +export { openSpotlight }; diff --git a/packages/spotlight/src/spotlight-store.ts b/packages/spotlight/src/spotlight-store.ts new file mode 100644 index 000000000..3e8659de4 --- /dev/null +++ b/packages/spotlight/src/spotlight-store.ts @@ -0,0 +1,45 @@ +"use client"; + +import { clamp } from "@mantine/hooks"; +import type { SpotlightStore } from "@mantine/spotlight"; +import { createSpotlight } from "@mantine/spotlight"; + +export const [spotlightStore, spotlightActions] = createSpotlight(); + +export const setSelectedAction = (index: number, store: SpotlightStore) => { + store.updateState((state) => ({ ...state, selected: index })); +}; + +export const selectAction = (index: number, store: SpotlightStore): number => { + const state = store.getState(); + const actionsList = document.getElementById(state.listId); + const selected = + actionsList?.querySelector("[data-selected]"); + const actions = + actionsList?.querySelectorAll("[data-action]") ?? []; + const nextIndex = + index === -1 ? actions.length - 1 : index === actions.length ? 0 : index; + + const selectedIndex = clamp(nextIndex, 0, actions.length - 1); + selected?.removeAttribute("data-selected"); + actions[selectedIndex]?.scrollIntoView({ block: "nearest" }); + actions[selectedIndex]?.setAttribute("data-selected", "true"); + setSelectedAction(selectedIndex, store); + + return selectedIndex; +}; + +export const selectNextAction = (store: SpotlightStore) => { + return selectAction(store.getState().selected + 1, store); +}; + +export const selectPreviousAction = (store: SpotlightStore) => { + return selectAction(store.getState().selected - 1, store); +}; +export const triggerSelectedAction = (store: SpotlightStore) => { + const state = store.getState(); + const selected = document.querySelector( + `#${state.listId} [data-selected]`, + ); + selected?.click(); +}; diff --git a/packages/spotlight/src/type.ts b/packages/spotlight/src/type.ts new file mode 100644 index 000000000..b8d72da87 --- /dev/null +++ b/packages/spotlight/src/type.ts @@ -0,0 +1,31 @@ +import type { + TranslationFunction, + TranslationObject, +} from "@homarr/translation"; +import type { TablerIconsProps } from "@homarr/ui"; + +export type SpotlightActionGroup = + keyof TranslationObject["common"]["search"]["group"]; + +interface BaseSpotlightAction { + id: string; + title: string | ((t: TranslationFunction) => string); + description: string | ((t: TranslationFunction) => string); + group: Exclude; // actions can not be assigned to the "all" group + icon: ((props: TablerIconsProps) => JSX.Element) | string; + ignoreSearchAndOnlyShowInGroup?: boolean; +} + +interface SpotlightActionLink extends BaseSpotlightAction { + type: "link"; + href: string; +} + +type MaybePromise = T | Promise; + +interface SpotlightActionButton extends BaseSpotlightAction { + type: "button"; + onClick: () => MaybePromise; +} + +export type SpotlightActionData = SpotlightActionLink | SpotlightActionButton; diff --git a/packages/translation/src/index.ts b/packages/translation/src/index.ts index 9fb09f4f6..dfc6ea084 100644 --- a/packages/translation/src/index.ts +++ b/packages/translation/src/index.ts @@ -1,3 +1,5 @@ +export * from "./type"; + export const supportedLanguages = ["en", "de"] as const; export type SupportedLanguage = (typeof supportedLanguages)[number]; diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index ea712acad..9ed6c20e9 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -154,6 +154,11 @@ export default { search: { placeholder: "Search for anything...", nothingFound: "Nothing found", + group: { + all: "All", + web: "Web", + action: "Actions", + }, }, userAvatar: { menu: { diff --git a/packages/translation/src/type.ts b/packages/translation/src/type.ts new file mode 100644 index 000000000..d673d652d --- /dev/null +++ b/packages/translation/src/type.ts @@ -0,0 +1,5 @@ +import type { useI18n } from "./client"; +import type enTranslation from "./lang/en"; + +export type TranslationFunction = ReturnType; +export type TranslationObject = typeof enTranslation; diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index cedc75b28..b2a7b4531 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -55,7 +55,6 @@ export const WidgetEditModal: ManagedModal> = ({ )} {Object.entries(definition.options).map( ([key, value]: [string, OptionsBuilderResult[string]]) => { - console.log(value); const Input = getInputForType(value.type); if (!Input || value.shouldHide?.(form.values.options as never)) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c14358620..999eaee6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,14 +106,14 @@ importers: specifier: ^0.9.2 version: 0.9.2(typescript@5.3.3)(zod@3.22.4) '@tanstack/react-query': - specifier: ^5.20.5 - version: 5.20.5(react@18.2.0) + specifier: ^5.21.2 + version: 5.21.2(react@18.2.0) '@tanstack/react-query-devtools': - specifier: ^5.21.0 - version: 5.21.0(@tanstack/react-query@5.20.5)(react@18.2.0) + specifier: ^5.21.3 + version: 5.21.3(@tanstack/react-query@5.21.2)(react@18.2.0) '@tanstack/react-query-next-experimental': - specifier: 5.20.5 - version: 5.20.5(@tanstack/react-query@5.20.5)(next@14.1.0)(react@18.2.0) + specifier: 5.21.2 + version: 5.21.2(@tanstack/react-query@5.21.2)(next@14.1.1-canary.58)(react@18.2.0) '@tiptap/extension-link': specifier: ^2.2.3 version: 2.2.3(@tiptap/core@2.2.3)(@tiptap/pm@2.2.3) @@ -128,10 +128,10 @@ importers: version: 11.0.0-next-beta.289(@trpc/server@11.0.0-next-beta.289) '@trpc/next': specifier: next - version: 11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/react-query@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) + version: 11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/react-query@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(next@14.1.1-canary.58)(react-dom@18.2.0)(react@18.2.0) '@trpc/react-query': specifier: next - version: 11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0) + version: 11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': specifier: next version: 11.0.0-next-beta.289 @@ -140,13 +140,13 @@ importers: version: 1.11.10 jotai: specifier: ^2.6.4 - version: 2.6.4(@types/react@18.2.55)(react@18.2.0) + version: 2.6.4(@types/react@18.2.56)(react@18.2.0) mantine-modal-manager: specifier: ^7.5.2 version: 7.5.2(@mantine/hooks@7.5.3)(react-dom@18.2.0)(react@18.2.0) next: - specifier: ^14.1.0 - version: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) + specifier: ^14.1.1-canary.58 + version: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) postcss-preset-mantine: specifier: ^1.13.0 version: 1.13.0(postcss@8.4.35) @@ -162,6 +162,9 @@ importers: superjson: specifier: 2.2.1 version: 2.2.1 + use-deep-compare-effect: + specifier: ^1.8.1 + version: 1.8.1(react@18.2.0) devDependencies: '@homarr/eslint-config': specifier: workspace:^0.2.0 @@ -176,8 +179,8 @@ importers: specifier: ^20.11.19 version: 20.11.19 '@types/react': - specifier: ^18.2.55 - version: 18.2.55 + specifier: ^18.2.56 + version: 18.2.56 '@types/react-dom': specifier: ^18.2.19 version: 18.2.19 @@ -258,11 +261,11 @@ importers: specifier: ^0.9.1 version: 0.9.1 next: - specifier: ^14.1.0 - version: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) + specifier: ^14.1.1-canary.58 + version: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) next-auth: specifier: 5.0.0-beta.11 - version: 5.0.0-beta.11(next@14.1.0)(react@18.2.0) + version: 5.0.0-beta.11(next@14.1.1-canary.58)(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -433,6 +436,12 @@ importers: packages/spotlight: dependencies: + '@homarr/translation': + specifier: workspace:^0.1.0 + version: link:../translation + '@homarr/ui': + specifier: workspace:^0.1.0 + version: link:../ui '@mantine/spotlight': specifier: ^7.5.3 version: 7.5.3(@mantine/core@7.5.3)(@mantine/hooks@7.5.3)(react-dom@18.2.0)(react@18.2.0) @@ -1659,8 +1668,8 @@ packages: - supports-color dev: false - /@next/env@14.1.0: - resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==} + /@next/env@14.1.1-canary.58: + resolution: {integrity: sha512-xMoPPiWVEIyIPoWaLhswlMq2Tnfkcv+XRmUAEhKkJgWdHIMFh45tiniWrZQLfrn4DAnF8gEAoejJaniNShFm7w==} dev: false /@next/eslint-plugin-next@14.1.0: @@ -1669,8 +1678,8 @@ packages: glob: 10.3.10 dev: false - /@next/swc-darwin-arm64@14.1.0: - resolution: {integrity: sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==} + /@next/swc-darwin-arm64@14.1.1-canary.58: + resolution: {integrity: sha512-exDpEe9ptK0NJEEyA+sThnR/6Oif/XaPK1y10fkcTNoQJkUCENL46nR1bCEYDAQrS5w03C1BHs3LSIOuoQcIag==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1678,8 +1687,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@14.1.0: - resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==} + /@next/swc-darwin-x64@14.1.1-canary.58: + resolution: {integrity: sha512-Q+38djBg+yaZjB8R6kst8kqHLIpTIM2RzuIvFGOflb8WaVI1ApXERTJKd345BOp42s0rIJ1UqynRplNCqYebeA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1687,8 +1696,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@14.1.0: - resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==} + /@next/swc-linux-arm64-gnu@14.1.1-canary.58: + resolution: {integrity: sha512-wv2457qPqh7ENqQ3t8sCWCsNStEW/o8qejZ6Ywdwl3eKP1jnFNtn4OWfOuP2FUl1XmVEzbABozuOyiCGAtD63Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1696,8 +1705,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@14.1.0: - resolution: {integrity: sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==} + /@next/swc-linux-arm64-musl@14.1.1-canary.58: + resolution: {integrity: sha512-xvfU/+mZfLpW/tRy743JtkNPlNI2t1dSkIzvDHHn/V28k1c+4YmsNxKMZRqqcxi73hwnuWYgor/96Sm4r9pmQQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1705,8 +1714,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@14.1.0: - resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==} + /@next/swc-linux-x64-gnu@14.1.1-canary.58: + resolution: {integrity: sha512-OgayNv+ChIjsPYMkBit2LTI0tMJ72or4w5DfQe2i1M2Ccy09p/LSRCiOptkV3XKyO+Tz8+fAqN+PdLRxfxQiuQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1714,8 +1723,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@14.1.0: - resolution: {integrity: sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==} + /@next/swc-linux-x64-musl@14.1.1-canary.58: + resolution: {integrity: sha512-6epwXWxSUyNKzOt0FSRW0QioPaWA9xlu8LM5N0c9eg8vaI3FvZmQegtHIzuzNc7RyWE1a5ZY4uDBVA1JaUJRow==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1723,8 +1732,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@14.1.0: - resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==} + /@next/swc-win32-arm64-msvc@14.1.1-canary.58: + resolution: {integrity: sha512-Qa79WuVRcrkmbxzTXr/tmWvFHVMjUu9KpxNrKn+HO/DmrVEnR4NxmuzOOxc6QkCb+Rb86rfV7tttPQqw7YigBA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -1732,8 +1741,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@14.1.0: - resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==} + /@next/swc-win32-ia32-msvc@14.1.1-canary.58: + resolution: {integrity: sha512-fuqdt6XKc4kvUpgOMh/LL2eqJ/eltxknClpaAg9Nt0dGTQIF0rjKEVm8NRwv832URZKKBbz9AtBp5TEmOquG9g==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -1741,8 +1750,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@14.1.0: - resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==} + /@next/swc-win32-x64-msvc@14.1.1-canary.58: + resolution: {integrity: sha512-P/DkKFiZo1mILb2UGt1YiurmuKo9LLvgC0l77YQ5g2b8jrTbYS9JeJJcdhLXnsHPPQrR/QgnEDJ2zWRtIhUJZA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1934,9 +1943,14 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true - /@swc/helpers@0.5.2: - resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: false + + /@swc/helpers@0.5.5: + resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} dependencies: + '@swc/counter': 0.1.3 tslib: 2.6.2 dev: false @@ -1981,43 +1995,43 @@ packages: resolution: {integrity: sha512-4w5evLh+7FUUiA1GucvGj2ReX2TvOjEr4ejXdwL/bsjoSkof6r1gQmzqI+VHrE2CpJpB3al7bCTulOkFa/RcyA==} dev: false - /@tanstack/query-core@5.20.5: - resolution: {integrity: sha512-T1W28gGgWn0A++tH3lxj3ZuUVZZorsiKcv+R50RwmPYz62YoDEkG4/aXHZELGkRp4DfrW07dyq2K5dvJ4Wl1aA==} + /@tanstack/query-core@5.21.2: + resolution: {integrity: sha512-jg7OcDG44oLT3uuGQQ9BM65ZBIdAq9xNZXPEk7Gr6w1oM1wo2/95H3dPDjLVs0yZKwrmE/ORUOC2Pyi1sp2fDA==} dev: false - /@tanstack/query-devtools@5.21.0: - resolution: {integrity: sha512-hbfuh9xredeehLhlJY38sey7Ezlr3KduDHRkrnvbwuTQCgkvixFdcJGMpUx0khh74q3Ay8QGa2uO+fV/kSNlww==} + /@tanstack/query-devtools@5.21.3: + resolution: {integrity: sha512-tRJ3uYpF8mLqb+Na25DqPsqdsK38Ff1HIse3znQIEoGMzf7rJKTwV9HCn/9cPQTyTcAuSUZOajyCVz7hnbdCdQ==} dev: false - /@tanstack/react-query-devtools@5.21.0(@tanstack/react-query@5.20.5)(react@18.2.0): - resolution: {integrity: sha512-79Rq5gtf9iuOANtrAIvSukC2ZT5d8iPXrJtCnkpJWKM2i+pIl6aBJR56sLJiFOvtlrJ1Cy8dkWZzWSi1s4tBbg==} + /@tanstack/react-query-devtools@5.21.3(@tanstack/react-query@5.21.2)(react@18.2.0): + resolution: {integrity: sha512-xU6nSshTneHppN9xQnjLgIAy36GHsQsHp/aCa515dRzcgEqC99Ix4TdwbduPSRTk5ZATsUEO0kKMZzSbubO0Pg==} peerDependencies: - '@tanstack/react-query': ^5.20.5 + '@tanstack/react-query': ^5.21.2 react: ^18.0.0 dependencies: - '@tanstack/query-devtools': 5.21.0 - '@tanstack/react-query': 5.20.5(react@18.2.0) + '@tanstack/query-devtools': 5.21.3 + '@tanstack/react-query': 5.21.2(react@18.2.0) react: 18.2.0 dev: false - /@tanstack/react-query-next-experimental@5.20.5(@tanstack/react-query@5.20.5)(next@14.1.0)(react@18.2.0): - resolution: {integrity: sha512-P4r357MckowLGUAeQJ9UWTBK4i/JS/G4alBuXkNNyDp8md/pzk/VXG4y+c6/kJWOoi/Qtawz122l4oMJFp3MHA==} + /@tanstack/react-query-next-experimental@5.21.2(@tanstack/react-query@5.21.2)(next@14.1.1-canary.58)(react@18.2.0): + resolution: {integrity: sha512-IKx6MvXxUWe2uAONZJZAsYBGhDXNpVoN5B81+JsS7AdOADO4+EiASk+3VPPg8vtkdYioiqbJ6sNp9zRn7pOGTA==} peerDependencies: - '@tanstack/react-query': ^5.20.5 + '@tanstack/react-query': ^5.21.2 next: ^13 || ^14 react: ^18.0.0 dependencies: - '@tanstack/react-query': 5.20.5(react@18.2.0) - next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) + '@tanstack/react-query': 5.21.2(react@18.2.0) + next: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) react: 18.2.0 dev: false - /@tanstack/react-query@5.20.5(react@18.2.0): - resolution: {integrity: sha512-6MHwJ8G9cnOC/XKrwt56QMc91vN7hLlAQNUA0ubP7h9Jj3a/CmkUwT6ALdFbnVP+PsYdhW3WONa8WQ4VcTaSLQ==} + /@tanstack/react-query@5.21.2(react@18.2.0): + resolution: {integrity: sha512-/Vv1qTumNDDVA5EYk40kivHZ2kICs1w38GBLRvV6A/lrixUJR5bfqZMKqHA1S6ND3gR9hvSyAAYejBbjLrQnSA==} peerDependencies: react: ^18.0.0 dependencies: - '@tanstack/query-core': 5.20.5 + '@tanstack/query-core': 5.21.2 react: 18.2.0 dev: false @@ -2314,7 +2328,7 @@ packages: '@trpc/server': 11.0.0-next-beta.289 dev: false - /@trpc/next@11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/react-query@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(next@14.1.0)(react-dom@18.2.0)(react@18.2.0): + /@trpc/next@11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/react-query@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(next@14.1.1-canary.58)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-AKCrcbtHh/zFrld6lMG0RC37d/aac4ZisLDjJcViMnEmJXCo0J5nhoZa6f+G9N683NdMWZVmY2rmJidw9IX3QQ==} peerDependencies: '@tanstack/react-query': ^5.0.0 @@ -2330,16 +2344,16 @@ packages: '@trpc/react-query': optional: true dependencies: - '@tanstack/react-query': 5.20.5(react@18.2.0) + '@tanstack/react-query': 5.21.2(react@18.2.0) '@trpc/client': 11.0.0-next-beta.289(@trpc/server@11.0.0-next-beta.289) - '@trpc/react-query': 11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0) + '@trpc/react-query': 11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0) '@trpc/server': 11.0.0-next-beta.289 - next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) + next: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@trpc/react-query@11.0.0-next-beta.289(@tanstack/react-query@5.20.5)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0): + /@trpc/react-query@11.0.0-next-beta.289(@tanstack/react-query@5.21.2)(@trpc/client@11.0.0-next-beta.289)(@trpc/server@11.0.0-next-beta.289)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-SAn09DmZ4eFYLS0cCHOVNvRHJhHZ2ssUj4LUTj56wym0MieaCSrcxTqiolnaMfF+mWc1SJlLOzebrxaTHPwJSw==} peerDependencies: '@tanstack/react-query': ^5.0.0 @@ -2348,7 +2362,7 @@ packages: react: '>=18.2.0' react-dom: '>=18.2.0' dependencies: - '@tanstack/react-query': 5.20.5(react@18.2.0) + '@tanstack/react-query': 5.21.2(react@18.2.0) '@trpc/client': 11.0.0-next-beta.289(@trpc/server@11.0.0-next-beta.289) '@trpc/server': 11.0.0-next-beta.289 react: 18.2.0 @@ -2587,11 +2601,11 @@ packages: /@types/react-dom@18.2.19: resolution: {integrity: sha512-aZvQL6uUbIJpjZk4U8JZGbau9KDeAwMfmhyWorxgBkqDIEf6ROjRozcmPIicqsUwPUjbkDfHKgGee1Lq65APcA==} dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.56 dev: true - /@types/react@18.2.55: - resolution: {integrity: sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==} + /@types/react@18.2.56: + resolution: {integrity: sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==} dependencies: '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.8 @@ -5378,7 +5392,7 @@ packages: resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==} dev: false - /jotai@2.6.4(@types/react@18.2.55)(react@18.2.0): + /jotai@2.6.4(@types/react@18.2.56)(react@18.2.0): resolution: {integrity: sha512-RniwQPX4893YlNR1muOtyUGHYaTD1fhEN4qnOuZJSrDHj6xdEMrqlRSN/hCm2fshwk78ruecB/P2l+NCVWe6TQ==} engines: {node: '>=12.20.0'} peerDependencies: @@ -5390,7 +5404,7 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.55 + '@types/react': 18.2.56 react: 18.2.0 dev: false @@ -5890,7 +5904,7 @@ packages: engines: {node: '>= 0.4.0'} dev: true - /next-auth@5.0.0-beta.11(next@14.1.0)(react@18.2.0): + /next-auth@5.0.0-beta.11(next@14.1.1-canary.58)(react@18.2.0): resolution: {integrity: sha512-OrfOVyXBGC69O8lEe81ZwFzQuWnY3Bsqee7kawr1iBycgSD64oNCtoFvRXiOWe7R0jOaafqQQlpsOS0mARhT3Q==} peerDependencies: '@simplewebauthn/browser': ^9.0.1 @@ -5907,7 +5921,7 @@ packages: optional: true dependencies: '@auth/core': 0.27.0 - next: 14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) + next: 14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0) react: 18.2.0 dev: false @@ -5923,8 +5937,8 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: true - /next@14.1.0(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0): - resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==} + /next@14.1.1-canary.58(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.0): + resolution: {integrity: sha512-rL4Tmm5ldL4VA+lTz0sQfJcDOe3vXB7LItcWuILb8eDs/6X1wrXNF9KDtviJH0tq/jrrTOihN7aBCkkge85m6g==} engines: {node: '>=18.17.0'} hasBin: true peerDependencies: @@ -5938,8 +5952,8 @@ packages: sass: optional: true dependencies: - '@next/env': 14.1.0 - '@swc/helpers': 0.5.2 + '@next/env': 14.1.1-canary.58 + '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001587 graceful-fs: 4.2.11 @@ -5949,15 +5963,15 @@ packages: sass: 1.71.0 styled-jsx: 5.1.1(@babel/core@7.23.9)(react@18.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 14.1.0 - '@next/swc-darwin-x64': 14.1.0 - '@next/swc-linux-arm64-gnu': 14.1.0 - '@next/swc-linux-arm64-musl': 14.1.0 - '@next/swc-linux-x64-gnu': 14.1.0 - '@next/swc-linux-x64-musl': 14.1.0 - '@next/swc-win32-arm64-msvc': 14.1.0 - '@next/swc-win32-ia32-msvc': 14.1.0 - '@next/swc-win32-x64-msvc': 14.1.0 + '@next/swc-darwin-arm64': 14.1.1-canary.58 + '@next/swc-darwin-x64': 14.1.1-canary.58 + '@next/swc-linux-arm64-gnu': 14.1.1-canary.58 + '@next/swc-linux-arm64-musl': 14.1.1-canary.58 + '@next/swc-linux-x64-gnu': 14.1.1-canary.58 + '@next/swc-linux-x64-musl': 14.1.1-canary.58 + '@next/swc-win32-arm64-msvc': 14.1.1-canary.58 + '@next/swc-win32-ia32-msvc': 14.1.1-canary.58 + '@next/swc-win32-x64-msvc': 14.1.1-canary.58 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -7847,6 +7861,17 @@ packages: react: 18.2.0 dev: false + /use-deep-compare-effect@1.8.1(react@18.2.0): + resolution: {integrity: sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13' + dependencies: + '@babel/runtime': 7.23.9 + dequal: 2.0.3 + react: 18.2.0 + dev: false + /use-isomorphic-layout-effect@1.1.2(react@18.2.0): resolution: {integrity: sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==} peerDependencies: