first commit
This commit is contained in:
14
calcom/packages/lib/hooks/useApp.ts
Normal file
14
calcom/packages/lib/hooks/useApp.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export default function useApp(appId: string) {
|
||||
const { status } = useSession();
|
||||
|
||||
return trpc.viewer.appById.useQuery(
|
||||
{ appId },
|
||||
{
|
||||
enabled: status === "authenticated",
|
||||
}
|
||||
);
|
||||
}
|
||||
7
calcom/packages/lib/hooks/useBookerUrl.ts
Normal file
7
calcom/packages/lib/hooks/useBookerUrl.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
|
||||
export const useBookerUrl = () => {
|
||||
const orgBranding = useOrgBranding();
|
||||
return orgBranding?.fullDomain ?? WEBSITE_URL ?? WEBAPP_URL;
|
||||
};
|
||||
14
calcom/packages/lib/hooks/useCallbackRef.ts
Normal file
14
calcom/packages/lib/hooks/useCallbackRef.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useRef } from "react";
|
||||
|
||||
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
|
||||
|
||||
export const useCallbackRef = <C>(callback: C) => {
|
||||
const callbackRef = useRef(callback);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
callbackRef.current = callback;
|
||||
});
|
||||
|
||||
return callbackRef;
|
||||
};
|
||||
|
||||
export default useCallbackRef;
|
||||
67
calcom/packages/lib/hooks/useCompatSearchParams.test.ts
Normal file
67
calcom/packages/lib/hooks/useCompatSearchParams.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { renderHook } from "@testing-library/react-hooks";
|
||||
import { vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { useCompatSearchParams } from "./useCompatSearchParams";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
ReadonlyURLSearchParams: vi.fn((a) => a),
|
||||
}));
|
||||
|
||||
describe("useCompatSearchParams hook", () => {
|
||||
it("should return the searchParams in next@13.4.6 Pages Router, SSR", async () => {
|
||||
const navigation = await import("next/navigation");
|
||||
|
||||
navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams("a=a&b=b"));
|
||||
navigation.useParams = vi.fn().mockReturnValue(null);
|
||||
|
||||
const { result } = renderHook(() => useCompatSearchParams());
|
||||
|
||||
expect(result.current.toString()).toEqual("a=a&b=b");
|
||||
});
|
||||
|
||||
it("should return both searchParams and params in next@13.4.6 App Router, SSR", async () => {
|
||||
const navigation = await import("next/navigation");
|
||||
|
||||
navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams("a=a"));
|
||||
navigation.useParams = vi.fn().mockReturnValue({ b: "b" });
|
||||
|
||||
const { result } = renderHook(() => useCompatSearchParams());
|
||||
|
||||
expect(result.current.toString()).toEqual("a=a&b=b");
|
||||
});
|
||||
|
||||
it("params should always override searchParams in case of conflicting keys", async () => {
|
||||
const navigation = await import("next/navigation");
|
||||
|
||||
navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams("a=a"));
|
||||
navigation.useParams = vi.fn().mockReturnValue({ a: "b" });
|
||||
|
||||
const { result } = renderHook(() => useCompatSearchParams());
|
||||
|
||||
expect(result.current.toString()).toEqual("a=b");
|
||||
});
|
||||
|
||||
it("should split paramsseparated with '/' (catch-all segments) in next@13.4.6 App Router, SSR", async () => {
|
||||
const navigation = await import("next/navigation");
|
||||
|
||||
navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams());
|
||||
// in next@13.4.6 useParams will return params separated by `/`
|
||||
navigation.useParams = vi.fn().mockReturnValue({ a: "a/b/c" });
|
||||
|
||||
const { result } = renderHook(() => useCompatSearchParams());
|
||||
|
||||
expect(result.current.getAll("a")).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("should include params and searchParams in next@13.5.4, Pages/App Router, SSR", async () => {
|
||||
const navigation = await import("next/navigation");
|
||||
|
||||
navigation.useSearchParams = vi.fn().mockReturnValue(new URLSearchParams("a=a"));
|
||||
navigation.useParams = vi.fn().mockReturnValue({ b: "b" });
|
||||
|
||||
const { result } = renderHook(() => useCompatSearchParams());
|
||||
|
||||
expect(result.current.toString()).toEqual("a=a&b=b");
|
||||
});
|
||||
});
|
||||
22
calcom/packages/lib/hooks/useCompatSearchParams.tsx
Normal file
22
calcom/packages/lib/hooks/useCompatSearchParams.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ReadonlyURLSearchParams, useParams, useSearchParams } from "next/navigation";
|
||||
|
||||
export const useCompatSearchParams = () => {
|
||||
const _searchParams = useSearchParams() ?? new URLSearchParams();
|
||||
const params = useParams() ?? {};
|
||||
|
||||
const searchParams = new URLSearchParams(_searchParams.toString());
|
||||
Object.getOwnPropertyNames(params).forEach((key) => {
|
||||
searchParams.delete(key);
|
||||
|
||||
// Though useParams is supposed to return a string/string[] as the key's value but it is found to return undefined as well.
|
||||
// Maybe it happens for pages dir when using optional catch-all routes.
|
||||
const param = params[key] || "";
|
||||
const paramArr = typeof param === "string" ? param.split("/") : param;
|
||||
|
||||
paramArr.forEach((p) => {
|
||||
searchParams.append(key, p);
|
||||
});
|
||||
});
|
||||
|
||||
return new ReadonlyURLSearchParams(searchParams);
|
||||
};
|
||||
27
calcom/packages/lib/hooks/useCopy.ts
Normal file
27
calcom/packages/lib/hooks/useCopy.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useCopy() {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => setIsCopied(true))
|
||||
.catch((error) => console.error("Copy to clipboard failed:", error));
|
||||
}
|
||||
};
|
||||
|
||||
const resetCopyStatus = () => {
|
||||
setIsCopied(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied) {
|
||||
const timer = setTimeout(resetCopyStatus, 3000); // Reset copy status after 3 seconds
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isCopied]);
|
||||
|
||||
return { isCopied, copyToClipboard, resetCopyStatus };
|
||||
}
|
||||
17
calcom/packages/lib/hooks/useDebounce.tsx
Normal file
17
calcom/packages/lib/hooks/useDebounce.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
42
calcom/packages/lib/hooks/useHasPaidPlan.ts
Normal file
42
calcom/packages/lib/hooks/useHasPaidPlan.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import { IS_SELF_HOSTED } from "../constants";
|
||||
import hasKeyInMetadata from "../hasKeyInMetadata";
|
||||
|
||||
export function useHasPaidPlan() {
|
||||
if (IS_SELF_HOSTED) return { isPending: false, hasPaidPlan: true };
|
||||
|
||||
const { data: hasTeamPlan, isPending: isPendingTeamQuery } = trpc.viewer.teams.hasTeamPlan.useQuery();
|
||||
|
||||
const { data: user, isPending: isPendingUserQuery } = trpc.viewer.me.useQuery();
|
||||
|
||||
const isPending = isPendingTeamQuery || isPendingUserQuery;
|
||||
|
||||
const isCurrentUsernamePremium =
|
||||
user && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
|
||||
|
||||
const hasPaidPlan = hasTeamPlan?.hasTeamPlan || isCurrentUsernamePremium;
|
||||
|
||||
return { isPending, hasPaidPlan };
|
||||
}
|
||||
|
||||
export function useTeamInvites() {
|
||||
const listInvites = trpc.viewer.teams.listInvites.useQuery();
|
||||
|
||||
return { isPending: listInvites.isPending, listInvites: listInvites.data };
|
||||
}
|
||||
|
||||
export function useHasTeamPlan() {
|
||||
const { data: hasTeamPlan, isPending } = trpc.viewer.teams.hasTeamPlan.useQuery();
|
||||
|
||||
return { isPending, hasTeamPlan: hasTeamPlan?.hasTeamPlan };
|
||||
}
|
||||
|
||||
export function useHasEnterprisePlan() {
|
||||
// TODO: figure out how to get "has Enterprise / has Org" from the backend
|
||||
const { data: hasTeamPlan, isPending } = trpc.viewer.teams.hasTeamPlan.useQuery();
|
||||
|
||||
return { isPending, hasTeamPlan: hasTeamPlan?.hasTeamPlan };
|
||||
}
|
||||
|
||||
export default useHasPaidPlan;
|
||||
3
calcom/packages/lib/hooks/useIsomorphicLayoutEffect.ts
Normal file
3
calcom/packages/lib/hooks/useIsomorphicLayoutEffect.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { useEffect, useLayoutEffect } from "react";
|
||||
|
||||
export const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
|
||||
47
calcom/packages/lib/hooks/useKeyPress.ts
Normal file
47
calcom/packages/lib/hooks/useKeyPress.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { RefObject } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useKeyPress(
|
||||
targetKey: string,
|
||||
ref?: RefObject<HTMLInputElement>,
|
||||
handler?: () => void
|
||||
): boolean {
|
||||
// State for keeping track of whether key is pressed
|
||||
const [keyPressed, setKeyPressed] = useState(false);
|
||||
const placeHolderRef = ref?.current;
|
||||
// If pressed key is our target key then set to true
|
||||
function downHandler({ key }: { key: string }) {
|
||||
if (key === targetKey) {
|
||||
setKeyPressed(true);
|
||||
handler && handler();
|
||||
}
|
||||
}
|
||||
// If released key is our target key then set to false
|
||||
const upHandler = ({ key }: { key: string }) => {
|
||||
if (key === targetKey) {
|
||||
setKeyPressed(false);
|
||||
}
|
||||
};
|
||||
// Add event listeners
|
||||
useEffect(() => {
|
||||
if (ref && placeHolderRef) {
|
||||
placeHolderRef.addEventListener("keydown", downHandler);
|
||||
placeHolderRef.addEventListener("keyup", upHandler);
|
||||
return () => {
|
||||
placeHolderRef?.removeEventListener("keydown", downHandler);
|
||||
placeHolderRef?.removeEventListener("keyup", upHandler);
|
||||
};
|
||||
} else {
|
||||
window.addEventListener("keydown", downHandler);
|
||||
window.addEventListener("keyup", upHandler);
|
||||
// Remove event listeners on cleanup
|
||||
return () => {
|
||||
window.removeEventListener("keydown", downHandler);
|
||||
window.removeEventListener("keyup", upHandler);
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Empty array ensures that effect is only run on mount and unmount
|
||||
|
||||
return keyPressed;
|
||||
}
|
||||
22
calcom/packages/lib/hooks/useLocale.ts
Normal file
22
calcom/packages/lib/hooks/useLocale.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import { useAtomsContext } from "@calcom/atoms/monorepo";
|
||||
|
||||
export const useLocale = (namespace: Parameters<typeof useTranslation>[0] = "common") => {
|
||||
const context = useAtomsContext();
|
||||
const { i18n, t } = useTranslation(namespace);
|
||||
const isLocaleReady = Object.keys(i18n).length > 0;
|
||||
|
||||
if (context?.clientId) {
|
||||
return { i18n: context.i18n, t: context.t, isLocaleReady: true } as unknown as {
|
||||
i18n: ReturnType<typeof useTranslation>["i18n"];
|
||||
t: ReturnType<typeof useTranslation>["t"];
|
||||
isLocaleReady: boolean;
|
||||
};
|
||||
}
|
||||
return {
|
||||
i18n,
|
||||
t,
|
||||
isLocaleReady,
|
||||
};
|
||||
};
|
||||
19
calcom/packages/lib/hooks/useMediaQuery.ts
Normal file
19
calcom/packages/lib/hooks/useMediaQuery.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const useMediaQuery = (query: string) => {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches);
|
||||
}
|
||||
const listener = () => setMatches(media.matches);
|
||||
window.addEventListener("resize", listener);
|
||||
return () => window.removeEventListener("resize", listener);
|
||||
}, [matches, query]);
|
||||
|
||||
return matches;
|
||||
};
|
||||
|
||||
export default useMediaQuery;
|
||||
25
calcom/packages/lib/hooks/useOnclickOutside.ts
Normal file
25
calcom/packages/lib/hooks/useOnclickOutside.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function useOnClickOutside(
|
||||
ref: React.RefObject<HTMLDivElement>,
|
||||
handler: (e?: MouseEvent | TouchEvent) => void
|
||||
) {
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent | TouchEvent) => {
|
||||
if (!ref.current || ref.current.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler(event);
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", listener);
|
||||
document.addEventListener("touchstart", listener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", listener);
|
||||
document.removeEventListener("touchstart", listener);
|
||||
};
|
||||
}, [ref, handler]);
|
||||
}
|
||||
28
calcom/packages/lib/hooks/usePagination.ts
Normal file
28
calcom/packages/lib/hooks/usePagination.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
import { useState, useMemo } from "react";
|
||||
|
||||
export function usePagination({
|
||||
defaultPageIndex = 1,
|
||||
defaultPageSize = 20,
|
||||
}: {
|
||||
defaultPageIndex?: number;
|
||||
defaultPageSize?: number;
|
||||
}) {
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: defaultPageIndex,
|
||||
pageSize: defaultPageSize,
|
||||
});
|
||||
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize]
|
||||
);
|
||||
|
||||
return {
|
||||
pagination,
|
||||
setPagination,
|
||||
};
|
||||
}
|
||||
63
calcom/packages/lib/hooks/useParamsWithFallback.test.ts
Normal file
63
calcom/packages/lib/hooks/useParamsWithFallback.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { renderHook } from "@testing-library/react-hooks";
|
||||
import { vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { useParamsWithFallback } from "./useParamsWithFallback";
|
||||
|
||||
describe("useParamsWithFallback hook", () => {
|
||||
it("should return router.query when param is null", () => {
|
||||
vi.mock("next/navigation", () => ({
|
||||
useParams: vi.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
vi.mock("next/compat/router", () => ({
|
||||
useRouter: vi.fn().mockReturnValue({ query: { id: 1 } }),
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useParamsWithFallback());
|
||||
|
||||
expect(result.current).toEqual({ id: 1 });
|
||||
});
|
||||
|
||||
it("should return router.query when param is undefined", () => {
|
||||
vi.mock("next/navigation", () => ({
|
||||
useParams: vi.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("next/compat/router", () => ({
|
||||
useRouter: vi.fn().mockReturnValue({ query: { id: 1 } }),
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useParamsWithFallback());
|
||||
|
||||
expect(result.current).toEqual({ id: 1 });
|
||||
});
|
||||
|
||||
it("should return useParams() if it exists", () => {
|
||||
vi.mock("next/navigation", () => ({
|
||||
useParams: vi.fn().mockReturnValue({ id: 1 }),
|
||||
}));
|
||||
|
||||
vi.mock("next/compat/router", () => ({
|
||||
useRouter: vi.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useParamsWithFallback());
|
||||
|
||||
expect(result.current).toEqual({ id: 1 });
|
||||
});
|
||||
|
||||
it("should return useParams() if it exists", () => {
|
||||
vi.mock("next/navigation", () => ({
|
||||
useParams: vi.fn().mockReturnValue({ id: 1 }),
|
||||
}));
|
||||
|
||||
vi.mock("next/compat/router", () => ({
|
||||
useRouter: vi.fn().mockReturnValue({ query: { id: 2 } }),
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useParamsWithFallback());
|
||||
|
||||
expect(result.current).toEqual({ id: 1 });
|
||||
});
|
||||
});
|
||||
18
calcom/packages/lib/hooks/useParamsWithFallback.ts
Normal file
18
calcom/packages/lib/hooks/useParamsWithFallback.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter as useCompatRouter } from "next/compat/router";
|
||||
import { useParams } from "next/navigation";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
|
||||
interface Params {
|
||||
[key: string]: string | string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is a workaround until pages are migrated to app directory.
|
||||
*/
|
||||
export function useParamsWithFallback(): Params | ParsedUrlQuery {
|
||||
const params = useParams(); // always `null` in pages router
|
||||
const router = useCompatRouter(); // always `null` in app router
|
||||
return params ?? router?.query ?? {};
|
||||
}
|
||||
25
calcom/packages/lib/hooks/useResponsive.tsx
Normal file
25
calcom/packages/lib/hooks/useResponsive.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const useResponsive = () => {
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
const handleWindowSizeChange = () => {
|
||||
setWidth(window.innerWidth);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", handleWindowSizeChange);
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleWindowSizeChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isSm: width <= 640,
|
||||
isMd: width > 640 && width <= 768,
|
||||
isLg: width > 768 && width <= 1024,
|
||||
isXl: width > 1024 && width <= 1280,
|
||||
is2xl: width > 1280 && width < 1536,
|
||||
};
|
||||
};
|
||||
|
||||
export default useResponsive;
|
||||
41
calcom/packages/lib/hooks/useRouterQuery.ts
Normal file
41
calcom/packages/lib/hooks/useRouterQuery.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
/**
|
||||
* An alternative to Object.fromEntries that allows duplicate keys.
|
||||
*/
|
||||
function fromEntriesWithDuplicateKeys(entries: IterableIterator<[string, string]> | null) {
|
||||
const result: Record<string, string | string[]> = {};
|
||||
|
||||
if (entries === null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Consider setting atleast ES2015 as target
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
for (const [key, value] of entries) {
|
||||
if (result.hasOwnProperty(key)) {
|
||||
let currentValue = result[key];
|
||||
if (!Array.isArray(currentValue)) {
|
||||
currentValue = [currentValue];
|
||||
}
|
||||
currentValue.push(value);
|
||||
result[key] = currentValue;
|
||||
} else {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook returns the query object from the router. It is an attempt to
|
||||
* keep the original query object from the old useRouter hook.
|
||||
* At least until everything is properly migrated to the new router.
|
||||
* @returns {Object} routerQuery
|
||||
*/
|
||||
export const useRouterQuery = () => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const routerQuery = fromEntriesWithDuplicateKeys(searchParams?.entries() ?? null);
|
||||
return routerQuery;
|
||||
};
|
||||
57
calcom/packages/lib/hooks/useTheme.tsx
Normal file
57
calcom/packages/lib/hooks/useTheme.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useTheme as useNextTheme } from "next-themes";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
|
||||
|
||||
/**
|
||||
* It should be called once per route if you intend to use a theme different from `system` theme. `system` theme is automatically supported using <ThemeProvider />
|
||||
* If needed you can also set system theme by passing 'system' as `themeToSet`
|
||||
* It handles embed configured theme automatically
|
||||
* To just read the values pass `getOnly` as `true` and `themeToSet` as `null`
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export default function useTheme(themeToSet: "system" | (string & {}) | undefined | null, getOnly = false) {
|
||||
themeToSet = themeToSet ? themeToSet : "system";
|
||||
const { resolvedTheme, setTheme, forcedTheme, theme: activeTheme } = useNextTheme();
|
||||
const embedTheme = useEmbedTheme();
|
||||
|
||||
useEffect(() => {
|
||||
// Undefined themeToSet allow the hook to be used where the theme is fetched after calling useTheme hook
|
||||
if (getOnly || themeToSet === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Embed theme takes precedence over theme configured in app.
|
||||
// If embedTheme isn't set i.e. it's not explicitly configured with a theme, then it would use the theme configured in appearance.
|
||||
// If embedTheme is set to "auto" then we consider it as null which then uses system theme.
|
||||
const finalThemeToSet = embedTheme ? (embedTheme === "auto" ? "system" : embedTheme) : themeToSet;
|
||||
|
||||
if (!finalThemeToSet || finalThemeToSet === activeTheme) return;
|
||||
|
||||
setTheme(finalThemeToSet);
|
||||
// We must not add `activeTheme` to the dependency list as it can cause an infinite loop b/w dark and theme switches
|
||||
// because there might be another booking page with conflicting theme.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [themeToSet, setTheme, embedTheme]);
|
||||
|
||||
if (getOnly) {
|
||||
return {
|
||||
resolvedTheme,
|
||||
forcedTheme,
|
||||
activeTheme,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently set theme values.
|
||||
*/
|
||||
export function useGetTheme() {
|
||||
const theme = useTheme(null, true);
|
||||
if (!theme) {
|
||||
throw new Error("useTheme must have a return value here");
|
||||
}
|
||||
return theme;
|
||||
}
|
||||
21
calcom/packages/lib/hooks/useTraceUpdate.tsx
Normal file
21
calcom/packages/lib/hooks/useTraceUpdate.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
export function useTraceUpdate(props: { [s: string]: unknown } | ArrayLike<unknown>) {
|
||||
const prev = useRef(props);
|
||||
useEffect(() => {
|
||||
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore TODO: fix this
|
||||
if (prev.current[k] !== v) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore TODO: fix this
|
||||
ps[k] = [prev.current[k], v];
|
||||
}
|
||||
return ps;
|
||||
}, {});
|
||||
if (Object.keys(changedProps).length > 0) {
|
||||
console.log("Changed props:", changedProps);
|
||||
}
|
||||
prev.current = props;
|
||||
});
|
||||
}
|
||||
124
calcom/packages/lib/hooks/useTypedQuery.ts
Normal file
124
calcom/packages/lib/hooks/useTypedQuery.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useCallback, useMemo, useEffect } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useRouterQuery } from "./useRouterQuery";
|
||||
|
||||
type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type FilteredKeys<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K];
|
||||
};
|
||||
|
||||
// Take array as a string and return zod array
|
||||
export const queryNumberArray = z
|
||||
.string()
|
||||
.or(z.number())
|
||||
.or(z.array(z.number()))
|
||||
.transform((a) => {
|
||||
if (typeof a === "string") return a.split(",").map((a) => Number(a));
|
||||
if (Array.isArray(a)) return a;
|
||||
return [a];
|
||||
});
|
||||
|
||||
// Take array as a string and return zod number array
|
||||
|
||||
// Take string and return return zod string array - comma separated
|
||||
export const queryStringArray = z
|
||||
.preprocess((a) => z.string().parse(a).split(","), z.string().array())
|
||||
.or(z.string().array());
|
||||
|
||||
export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
|
||||
type Output = z.infer<typeof schema>;
|
||||
type FullOutput = Required<Output>;
|
||||
type OutputKeys = Required<keyof FullOutput>;
|
||||
type OutputOptionalKeys = OptionalKeys<Output>;
|
||||
type ArrayOutput = FilteredKeys<FullOutput, Array<unknown>>;
|
||||
type ArrayOutputKeys = keyof ArrayOutput;
|
||||
const router = useRouter();
|
||||
const unparsedQuery = useRouterQuery();
|
||||
const pathname = usePathname();
|
||||
const parsedQuerySchema = schema.safeParse(unparsedQuery);
|
||||
let parsedQuery: Output = useMemo(() => {
|
||||
return {} as Output;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (parsedQuerySchema.success && parsedQuerySchema.data) {
|
||||
Object.entries(parsedQuerySchema.data).forEach(([key, value]) => {
|
||||
if (key in unparsedQuery || !value) return;
|
||||
const search = new URLSearchParams(parsedQuery);
|
||||
search.set(String(key), String(value));
|
||||
router.replace(`${pathname}?${search.toString()}`);
|
||||
});
|
||||
}
|
||||
}, [parsedQuerySchema, schema, router, pathname, unparsedQuery, parsedQuery]);
|
||||
|
||||
if (parsedQuerySchema.success) parsedQuery = parsedQuerySchema.data;
|
||||
else if (!parsedQuerySchema.success) console.error(parsedQuerySchema.error);
|
||||
|
||||
// Set the query based on schema values
|
||||
const setQuery = useCallback(
|
||||
function setQuery<J extends OutputKeys>(key: J, value: Output[J]) {
|
||||
// Remove old value by key so we can merge new value
|
||||
const search = new URLSearchParams(parsedQuery);
|
||||
search.set(String(key), String(value));
|
||||
router.replace(`${pathname}?${search.toString()}`);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[parsedQuery, router]
|
||||
);
|
||||
|
||||
// Delete a key from the query
|
||||
function removeByKey(key: OutputOptionalKeys) {
|
||||
const search = new URLSearchParams(parsedQuery);
|
||||
search.delete(String(key));
|
||||
router.replace(`${pathname}?${search.toString()}`);
|
||||
}
|
||||
|
||||
// push item to existing key
|
||||
function pushItemToKey<J extends ArrayOutputKeys>(key: J, value: ArrayOutput[J][number]) {
|
||||
const existingValue = parsedQuery[key];
|
||||
if (Array.isArray(existingValue)) {
|
||||
if (existingValue.includes(value)) return; // prevent adding the same value to the array
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setQuery(key, [...existingValue, value]);
|
||||
} else {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setQuery(key, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove item by key and value
|
||||
function removeItemByKeyAndValue<J extends ArrayOutputKeys>(key: J, value: ArrayOutput[J][number]) {
|
||||
const existingValue = parsedQuery[key];
|
||||
if (Array.isArray(existingValue) && existingValue.length > 1) {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
const newValue = existingValue.filter((item) => item !== value);
|
||||
setQuery(key, newValue);
|
||||
} else {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
removeByKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all query params from the URL
|
||||
function removeAllQueryParams() {
|
||||
if (pathname !== null) {
|
||||
router.replace(pathname);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: parsedQuery,
|
||||
setQuery,
|
||||
removeByKey,
|
||||
pushItemToKey,
|
||||
removeItemByKeyAndValue,
|
||||
removeAllQueryParams,
|
||||
};
|
||||
}
|
||||
21
calcom/packages/lib/hooks/useUrlMatchesCurrentUrl.ts
Normal file
21
calcom/packages/lib/hooks/useUrlMatchesCurrentUrl.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
|
||||
export const useUrlMatchesCurrentUrl = (url: string) => {
|
||||
// I don't know why usePathname ReturnType doesn't include null.
|
||||
// It can certainly have null value https://nextjs.org/docs/app/api-reference/functions/use-pathname#:~:text=usePathname%20can%20return%20null%20when%20a%20fallback%20route%20is%20being%20rendered%20or%20when%20a%20pages%20directory%20page%20has%20been%20automatically%20statically%20optimized%20by%20Next.js%20and%20the%20router%20is%20not%20ready.
|
||||
const pathname = usePathname() as null | string;
|
||||
const searchParams = useCompatSearchParams();
|
||||
const query = searchParams?.toString();
|
||||
let pathnameWithQuery;
|
||||
if (query) {
|
||||
pathnameWithQuery = `${pathname}?${query}`;
|
||||
} else {
|
||||
pathnameWithQuery = pathname;
|
||||
}
|
||||
// TODO: It should actually re-order the params before comparing ?a=1&b=2 should match with ?b=2&a=1
|
||||
return pathnameWithQuery ? pathnameWithQuery.includes(url) : false;
|
||||
};
|
||||
Reference in New Issue
Block a user