From dd53ccb9624e719d019a8fe29fcd66415c1b1528 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Wed, 20 Aug 2025 15:57:34 +0300 Subject: deps: Upgrade expo & nextjs to react 19 (#1565) * Attempt to upgrade expo 53 * Attempt upgrade nextjs * Fix a bunch of peer deps * upgrade some docs deps * fix typecheck * update the shadcn calendar component * more fixes * more fixes * revert ollama upgrade * update react version to use carets * remove react-select from landing * fix the typescript error caused by customFetch * upgrade the new grid user setting to nextjs 15 * mobile: enable react canary to support react 19.1 * upgrade react native menu * fix navigation context error --- apps/web/app/admin/layout.tsx | 3 +- .../@modal/(.)preview/[bookmarkId]/page.tsx | 9 +- apps/web/app/dashboard/feeds/[feedId]/page.tsx | 7 +- apps/web/app/dashboard/lists/[listId]/page.tsx | 13 +- .../app/dashboard/preview/[bookmarkId]/page.tsx | 7 +- apps/web/app/dashboard/tags/[tagId]/page.tsx | 13 +- apps/web/app/public/lists/[listId]/page.tsx | 14 +- apps/web/app/settings/layout.tsx | 2 +- .../components/dashboard/bookmarks/EditorCard.tsx | 4 +- .../components/dashboard/search/SearchInput.tsx | 2 +- apps/web/components/shared/sidebar/TSidebarItem.ts | 2 +- apps/web/components/theme-provider.tsx | 2 +- apps/web/components/ui/calendar.tsx | 230 +++++++++++++++++---- .../components/ui/markdown/markdown-readonly.tsx | 1 - .../web/lib/userLocalSettings/userLocalSettings.ts | 14 +- apps/web/next-env.d.ts | 2 +- apps/web/package.json | 20 +- apps/web/server/api/client.ts | 2 +- 18 files changed, 241 insertions(+), 106 deletions(-) (limited to 'apps/web') diff --git a/apps/web/app/admin/layout.tsx b/apps/web/app/admin/layout.tsx index 62a6932a..4b589712 100644 --- a/apps/web/app/admin/layout.tsx +++ b/apps/web/app/admin/layout.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { redirect } from "next/navigation"; import { AdminNotices } from "@/components/admin/AdminNotices"; import MobileSidebar from "@/components/shared/sidebar/MobileSidebar"; @@ -11,7 +12,7 @@ const adminSidebarItems = ( t: TFunction, ): { name: string; - icon: JSX.Element; + icon: React.ReactElement; path: string; }[] => [ { diff --git a/apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx b/apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx index 432e7a6c..77d84ec5 100644 --- a/apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx +++ b/apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx @@ -1,15 +1,14 @@ "use client"; -import { useState } from "react"; +import { use, useState } from "react"; import { useRouter } from "next/navigation"; import BookmarkPreview from "@/components/dashboard/preview/BookmarkPreview"; import { Dialog, DialogContent } from "@/components/ui/dialog"; -export default function BookmarkPreviewPage({ - params, -}: { - params: { bookmarkId: string }; +export default function BookmarkPreviewPage(props: { + params: Promise<{ bookmarkId: string }>; }) { + const params = use(props.params); const router = useRouter(); const [open, setOpen] = useState(true); diff --git a/apps/web/app/dashboard/feeds/[feedId]/page.tsx b/apps/web/app/dashboard/feeds/[feedId]/page.tsx index ed5f9e40..a73f0f32 100644 --- a/apps/web/app/dashboard/feeds/[feedId]/page.tsx +++ b/apps/web/app/dashboard/feeds/[feedId]/page.tsx @@ -3,11 +3,10 @@ import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; -export default async function FeedPage({ - params, -}: { - params: { feedId: string }; +export default async function FeedPage(props: { + params: Promise<{ feedId: string }>; }) { + const params = await props.params; let feed; try { feed = await api.feeds.get({ feedId: params.feedId }); diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx index de0f5054..4714a71c 100644 --- a/apps/web/app/dashboard/lists/[listId]/page.tsx +++ b/apps/web/app/dashboard/lists/[listId]/page.tsx @@ -6,15 +6,14 @@ import { TRPCError } from "@trpc/server"; import { BookmarkListContextProvider } from "@karakeep/shared-react/hooks/bookmark-list-context"; -export default async function ListPage({ - params, - searchParams, -}: { - params: { listId: string }; - searchParams?: { +export default async function ListPage(props: { + params: Promise<{ listId: string }>; + searchParams?: Promise<{ includeArchived?: string; - }; + }>; }) { + const searchParams = await props.searchParams; + const params = await props.params; const userSettings = await api.users.settings(); let list; try { diff --git a/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx b/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx index 236f5447..ea509207 100644 --- a/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx +++ b/apps/web/app/dashboard/preview/[bookmarkId]/page.tsx @@ -3,11 +3,10 @@ import BookmarkPreview from "@/components/dashboard/preview/BookmarkPreview"; import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; -export default async function BookmarkPreviewPage({ - params, -}: { - params: { bookmarkId: string }; +export default async function BookmarkPreviewPage(props: { + params: Promise<{ bookmarkId: string }>; }) { + const params = await props.params; let bookmark; try { bookmark = await api.bookmarks.getBookmark({ diff --git a/apps/web/app/dashboard/tags/[tagId]/page.tsx b/apps/web/app/dashboard/tags/[tagId]/page.tsx index b33a351a..7971da1e 100644 --- a/apps/web/app/dashboard/tags/[tagId]/page.tsx +++ b/apps/web/app/dashboard/tags/[tagId]/page.tsx @@ -7,15 +7,14 @@ import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; import { MoreHorizontal } from "lucide-react"; -export default async function TagPage({ - params, - searchParams, -}: { - params: { tagId: string }; - searchParams?: { +export default async function TagPage(props: { + params: Promise<{ tagId: string }>; + searchParams?: Promise<{ includeArchived?: string; - }; + }>; }) { + const searchParams = await props.searchParams; + const params = await props.params; let tag; try { tag = await api.tags.get({ tagId: params.tagId }); diff --git a/apps/web/app/public/lists/[listId]/page.tsx b/apps/web/app/public/lists/[listId]/page.tsx index 4a4ce414..17e6b947 100644 --- a/apps/web/app/public/lists/[listId]/page.tsx +++ b/apps/web/app/public/lists/[listId]/page.tsx @@ -6,11 +6,10 @@ import PublicListHeader from "@/components/public/lists/PublicListHeader"; import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; -export async function generateMetadata({ - params, -}: { - params: { listId: string }; +export async function generateMetadata(props: { + params: Promise<{ listId: string }>; }): Promise { + const params = await props.params; try { const resp = await api.publicBookmarks.getPublicListMetadata({ listId: params.listId, @@ -38,11 +37,10 @@ export async function generateMetadata({ }; } -export default async function PublicListPage({ - params, -}: { - params: { listId: string }; +export default async function PublicListPage(props: { + params: Promise<{ listId: string }>; }) { + const params = await props.params; try { const { list, bookmarks, nextCursor } = await api.publicBookmarks.getPublicBookmarksInList({ diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx index 94a5c4d7..982ac61a 100644 --- a/apps/web/app/settings/layout.tsx +++ b/apps/web/app/settings/layout.tsx @@ -25,7 +25,7 @@ const settingsSidebarItems = ( t: TFunction, ): { name: string; - icon: JSX.Element; + icon: React.ReactElement; path: string; }[] => { return [ diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx index a5966845..7ac1cade 100644 --- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx +++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx @@ -24,7 +24,9 @@ import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { useUploadAsset } from "../UploadDropzone"; -function useFocusOnKeyPress(inputRef: React.RefObject) { +function useFocusOnKeyPress( + inputRef: React.RefObject, +) { useEffect(() => { function handleKeyPress(e: KeyboardEvent) { if (!inputRef.current) { diff --git a/apps/web/components/dashboard/search/SearchInput.tsx b/apps/web/components/dashboard/search/SearchInput.tsx index 0de7694a..dad995e1 100644 --- a/apps/web/components/dashboard/search/SearchInput.tsx +++ b/apps/web/components/dashboard/search/SearchInput.tsx @@ -35,7 +35,7 @@ import QueryExplainerTooltip from "./QueryExplainerTooltip"; const MAX_DISPLAY_SUGGESTIONS = 5; function useFocusSearchOnKeyPress( - inputRef: React.RefObject, + inputRef: React.RefObject, value: string, setValue: (value: string) => void, setPopoverOpen: React.Dispatch>, diff --git a/apps/web/components/shared/sidebar/TSidebarItem.ts b/apps/web/components/shared/sidebar/TSidebarItem.ts index 84cd58f5..a1ea4c97 100644 --- a/apps/web/components/shared/sidebar/TSidebarItem.ts +++ b/apps/web/components/shared/sidebar/TSidebarItem.ts @@ -1,5 +1,5 @@ export interface TSidebarItem { name: string; - icon: JSX.Element; + icon: React.ReactElement; path: string; } diff --git a/apps/web/components/theme-provider.tsx b/apps/web/components/theme-provider.tsx index 737e1356..1ab9a49d 100644 --- a/apps/web/components/theme-provider.tsx +++ b/apps/web/components/theme-provider.tsx @@ -1,6 +1,6 @@ "use client"; -import type { ThemeProviderProps } from "next-themes/dist/types"; +import type { ThemeProviderProps } from "next-themes"; import * as React from "react"; import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes"; diff --git a/apps/web/components/ui/calendar.tsx b/apps/web/components/ui/calendar.tsx index 99a082f6..e2a13e9e 100644 --- a/apps/web/components/ui/calendar.tsx +++ b/apps/web/components/ui/calendar.tsx @@ -1,69 +1,209 @@ "use client"; import * as React from "react"; -import { buttonVariants } from "@/components/ui/button"; +import { Button, buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import { DayPicker } from "react-day-picker"; - -export type CalendarProps = React.ComponentProps; +import { + ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "lucide-react"; +import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"; function Calendar({ className, classNames, showOutsideDays = true, + captionLayout = "label", + buttonVariant = "ghost", + formatters, + components, ...props -}: CalendarProps) { +}: React.ComponentProps & { + buttonVariant?: React.ComponentProps["variant"]; +}) { + const defaultClassNames = getDefaultClassNames(); + return ( svg]:rotate-180`, + String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, + className, + )} + captionLayout={captionLayout} + formatters={{ + formatMonthDropdown: (date) => + date.toLocaleString("default", { month: "short" }), + ...formatters, + }} classNames={{ - months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", - month: "space-y-4", - caption: "flex justify-center pt-1 relative items-center", - caption_label: "text-sm font-medium", - nav: "space-x-1 flex items-center", - nav_button: cn( - buttonVariants({ variant: "outline" }), - "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", - ), - nav_button_previous: "absolute left-1", - nav_button_next: "absolute right-1", - table: "w-full border-collapse space-y-1", - head_row: "flex", - head_cell: - "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", - row: "flex w-full mt-2", - cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", + root: cn("w-fit", defaultClassNames.root), + months: cn( + "relative flex flex-col gap-4 md:flex-row", + defaultClassNames.months, + ), + month: cn("flex w-full flex-col gap-4", defaultClassNames.month), + nav: cn( + "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", + defaultClassNames.nav, + ), + button_previous: cn( + buttonVariants({ variant: buttonVariant }), + "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", + defaultClassNames.button_previous, + ), + button_next: cn( + buttonVariants({ variant: buttonVariant }), + "h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50", + defaultClassNames.button_next, + ), + month_caption: cn( + "flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]", + defaultClassNames.month_caption, + ), + dropdowns: cn( + "flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium", + defaultClassNames.dropdowns, + ), + dropdown_root: cn( + "has-focus:border-ring shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border border-input", + defaultClassNames.dropdown_root, + ), + dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown), + caption_label: cn( + "select-none font-medium", + captionLayout === "label" + ? "text-sm" + : "flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground", + defaultClassNames.caption_label, + ), + table: "w-full border-collapse", + weekdays: cn("flex", defaultClassNames.weekdays), + weekday: cn( + "flex-1 select-none rounded-md text-[0.8rem] font-normal text-muted-foreground", + defaultClassNames.weekday, + ), + week: cn("mt-2 flex w-full", defaultClassNames.week), + week_number_header: cn( + "w-[--cell-size] select-none", + defaultClassNames.week_number_header, + ), + week_number: cn( + "select-none text-[0.8rem] text-muted-foreground", + defaultClassNames.week_number, + ), day: cn( - buttonVariants({ variant: "ghost" }), - "h-9 w-9 p-0 font-normal aria-selected:opacity-100", - ), - day_range_end: "day-range-end", - day_selected: - "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", - day_today: "bg-accent text-accent-foreground", - day_outside: - "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", - day_disabled: "text-muted-foreground opacity-50", - day_range_middle: - "aria-selected:bg-accent aria-selected:text-accent-foreground", - day_hidden: "invisible", + "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md", + defaultClassNames.day, + ), + range_start: cn( + "rounded-l-md bg-accent", + defaultClassNames.range_start, + ), + range_middle: cn("rounded-none", defaultClassNames.range_middle), + range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end), + today: cn( + "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none", + defaultClassNames.today, + ), + outside: cn( + "text-muted-foreground aria-selected:text-muted-foreground", + defaultClassNames.outside, + ), + disabled: cn( + "text-muted-foreground opacity-50", + defaultClassNames.disabled, + ), + hidden: cn("invisible", defaultClassNames.hidden), ...classNames, }} components={{ - IconLeft: ({ className, ...props }) => ( - - ), - IconRight: ({ className, ...props }) => ( - - ), + Root: ({ className, rootRef, ...props }) => { + return ( +
+ ); + }, + Chevron: ({ className, orientation, ...props }) => { + if (orientation === "left") { + return ( + + ); + } + + if (orientation === "right") { + return ( + + ); + } + + return ( + + ); + }, + DayButton: CalendarDayButton, + WeekNumber: ({ children, ...props }) => { + return ( + +
+ {children} +
+ + ); + }, + ...components, }} {...props} /> ); } -Calendar.displayName = "Calendar"; -export { Calendar }; +function CalendarDayButton({ + className, + day, + modifiers, + ...props +}: React.ComponentProps) { + const defaultClassNames = getDefaultClassNames(); + + const ref = React.useRef(null); + React.useEffect(() => { + if (modifiers.focused) ref.current?.focus(); + }, [modifiers.focused]); + + return ( +