Merge branch 'dev' of https://github.com/homarr-labs/homarr into dev
This commit is contained in:
@@ -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 (
|
||||
<Container>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
8
apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx
Normal file
8
apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { Provider } from "jotai";
|
||||
|
||||
export const JotaiProvider = ({ children }: PropsWithChildren) => {
|
||||
return <Provider>{children}</Provider>;
|
||||
};
|
||||
@@ -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: {
|
||||
</clientApi.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
|
||||
18
apps/nextjs/src/app/[locale]/compose.tsx
Normal file
18
apps/nextjs/src/app/[locale]/compose.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
type PropsWithChildren = Required<React.PropsWithChildren>;
|
||||
|
||||
export const composeWrappers = (
|
||||
wrappers: React.FunctionComponent<PropsWithChildren>[],
|
||||
): React.FunctionComponent<PropsWithChildren> => {
|
||||
return wrappers
|
||||
.reverse()
|
||||
.reduce((Acc, Current): React.FunctionComponent<PropsWithChildren> => {
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (props) => (
|
||||
<Current>
|
||||
<Acc {...props} />
|
||||
</Current>
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -30,7 +30,6 @@ export const InitUserForm = () => {
|
||||
});
|
||||
|
||||
const handleSubmit = async (values: FormType) => {
|
||||
console.log(values);
|
||||
await mutateAsync(values, {
|
||||
onSuccess: () => {
|
||||
showSuccessNotification({
|
||||
|
||||
@@ -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) => <JotaiProvider {...innerProps} />,
|
||||
(innerProps) => <TRPCReactProvider {...innerProps} />,
|
||||
(innerProps) => (
|
||||
<NextInternationalProvider {...innerProps} locale={props.params.locale} />
|
||||
),
|
||||
(innerProps) => (
|
||||
<MantineProvider
|
||||
{...innerProps}
|
||||
defaultColorScheme={colorScheme}
|
||||
{...uiConfiguration}
|
||||
/>
|
||||
),
|
||||
(innerProps) => <ModalsProvider {...innerProps} />,
|
||||
]);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<ColorSchemeScript defaultColorScheme={colorScheme} />
|
||||
</head>
|
||||
<body className={["font-sans", fontSans.variable].join(" ")}>
|
||||
<TRPCReactProvider headers={headers()}>
|
||||
<NextInternationalProvider locale={props.params.locale}>
|
||||
<MantineProvider
|
||||
defaultColorScheme={colorScheme}
|
||||
{...uiConfiguration}
|
||||
>
|
||||
<ModalsProvider>
|
||||
<Notifications />
|
||||
{props.children}
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</NextInternationalProvider>
|
||||
</TRPCReactProvider>
|
||||
<StackedProvider>
|
||||
<Notifications />
|
||||
{props.children}
|
||||
</StackedProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -34,26 +34,28 @@ interface Props {
|
||||
export const SectionContent = ({ items, refs }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="grid-stack-item"
|
||||
data-id={item.id}
|
||||
gs-x={item.xOffset}
|
||||
gs-y={item.yOffset}
|
||||
gs-w={item.width}
|
||||
gs-h={item.height}
|
||||
gs-min-w={1}
|
||||
gs-min-h={1}
|
||||
gs-max-w={4}
|
||||
gs-max-h={4}
|
||||
ref={refs.items.current[item.id] as RefObject<HTMLDivElement>}
|
||||
>
|
||||
<Card className="grid-stack-item-content" withBorder>
|
||||
<BoardItem item={item} />
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="grid-stack-item"
|
||||
data-id={item.id}
|
||||
gs-x={item.xOffset}
|
||||
gs-y={item.yOffset}
|
||||
gs-w={item.width}
|
||||
gs-h={item.height}
|
||||
gs-min-w={1}
|
||||
gs-min-h={1}
|
||||
gs-max-w={4}
|
||||
gs-max-h={4}
|
||||
ref={refs.items.current[item.id] as RefObject<HTMLDivElement>}
|
||||
>
|
||||
<Card className="grid-stack-item-content" withBorder>
|
||||
<BoardItem item={item} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
<UserButton />
|
||||
</Group>
|
||||
</Group>
|
||||
<ClientSpotlight />
|
||||
<Spotlight />
|
||||
</AppShellHeader>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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={<IconSearch size={20} stroke={1.5} />}
|
||||
onClick={spotlight.open}
|
||||
onClick={openSpotlight}
|
||||
>
|
||||
{t("placeholder")}
|
||||
</TextInput>
|
||||
@@ -26,7 +26,7 @@ export const DesktopSearchInput = () => {
|
||||
|
||||
export const MobileSearchButton = () => {
|
||||
return (
|
||||
<HeaderButton onClick={spotlight.open} className={classes.mobileSearch}>
|
||||
<HeaderButton onClick={openSpotlight} className={classes.mobileSearch}>
|
||||
<IconSearch size={20} stroke={1.5} />
|
||||
</HeaderButton>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Spotlight
|
||||
actions={[]}
|
||||
nothingFound={t("nothingFound")}
|
||||
highlightQuery
|
||||
searchProps={{
|
||||
leftSection: <IconSearch size={20} stroke={1.5} />,
|
||||
placeholder: `${t("placeholder")}`,
|
||||
}}
|
||||
yOffset={12}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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<typeof appRouter>({
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user